最近有一个项目,客户要求根据经纬度将一些标注渲染在图片上,但是客户没有提供geoJson数据,如果没有geoJson那么就没办法将标注渲染到图片准确的位置上。为了能够准确的将标注渲染到坐标上,我们需要根据图片来想办法获取到geoJson数据。
因为这是一个小区域的坐标范围,并不是常规的省市县范围,所以我们没有办法找现成的geoJson,只能手动获取。
实现思路:
根据图片,我们可以看到,在图片左上角有一个谷脚镇,那么我们就以谷脚镇为基准将图片放在地图上进行匹配,获取图片在地图的范围,
然后再将图片渲染到地图上,勾勒出轮廓获取到geoJson坐标。
今天我们先实现第一步,将图片放在地图上获取到图片在地图上的范围坐标。
我们没有现成的工具来辅助,只能自己实现这些功能了。
在线演示地址
实例代码
我们通过我们的小工具来获取图片的坐标,为后期勾勒地图做准备。这里的地图我们使用的高德地图。这里图片不准确和实际地图轮廓是有出入的,客户那边只要求有个大概范围即可,不要求很准确,所以这里图片也就大概的范围,没办法精确.
[[106.871203,26.510772],[106.90369,26.507202],[106.899229,26.474684],[106.866742,26.478254]]
实例代码
<template>
<div class="gdMap">
<div class="container" ref="mapContainer" id="container"></div>
<img v-if="customImage" :src="customImage" :style="imageStyle" class="custom-image" @mousedown.prevent.stop="startDrag" @touchstart.prevent.stop="startDrag" @dragstart.prevent />
<div class="operation">
<h2>地图图片配准工具</h2>
<el-row class="mt20">
<el-col :span="8">
<span class="label">搜索地名:</span>
</el-col>
<el-col :span="16">
<el-input v-model="locationName" placeholder="请输入地名"></el-input>
</el-col>
</el-row>
<el-row class="mt20">
<el-col :span="24">
<el-button type="primary" @click="searchLocation">搜索</el-button>
</el-col>
</el-row>
<el-row class="mt20">
<el-col :span="24">
<el-upload
class="upload-demo"
action="#"
:on-change="handleImageChange"
:auto-upload="false"
>
<el-button type="primary">选择图片</el-button>
</el-upload>
</el-col>
</el-row>
<el-row class="mt20" v-if="customImage">
<el-col :span="8">
<span class="label">透明度:</span>
</el-col>
<el-col :span="16">
<el-slider v-model="opacity" :min="0" :max="1" :step="0.1"></el-slider>
</el-col>
</el-row>
<el-row class="mt20" v-if="customImage">
<el-col :span="8">
<span class="label">缩放:</span>
</el-col>
<el-col :span="10">
<el-slider v-model="scale" :min="0.001" :max="2" :step="0.001"></el-slider>
</el-col>
<el-col :span="6">
<el-input v-model.number="scale" :min="0.001" :max="2" :step="0.001" type="number"></el-input>
</el-col>
</el-row>
<el-row class="mt20" v-if="customImage">
<el-col :span="8">
<span class="label">左边距:</span>
</el-col>
<el-col :span="16">
<el-input v-model.number="imageLeft" :step="0.01" type="number" @change="updateImagePosition"></el-input>
</el-col>
</el-row>
<el-row class="mt20" v-if="customImage">
<el-col :span="8">
<span class="label">上边距:</span>
</el-col>
<el-col :span="16">
<el-input v-model.number="imageTop" :step="0.01" type="number" @change="updateImagePosition"></el-input>
</el-col>
</el-row>
<!-- 在缩放控件后添加旋转控件 -->
<el-row class="mt20" v-if="customImage">
<el-col :span="8">
<span class="label">旋转:</span>
</el-col>
<el-col :span="10">
<el-slider v-model="rotation" :min="0" :max="360" :step="1"></el-slider>
</el-col>
<el-col :span="6">
<el-input v-model.number="rotation" :min="0" :max="360" :step="1" type="number"></el-input>
</el-col>
</el-row>
<el-row class="mt20">
<el-col :span="8">
<span class="label">地图缩放:</span>
</el-col>
<el-col :span="16">
<el-input v-model.number="mapZoom" :step="0.001" type="number" @change="updateMapZoom"></el-input>
</el-col>
</el-row>
<el-row class="mt20" v-if="customImage">
<el-col :span="24">
<el-checkbox v-model="isDraggable">可拖动</el-checkbox>
</el-col>
</el-row>
<el-row class="mt20" v-if="customImage">
<el-col :span="24">
<el-button type="primary" @click="getCornerCoordinates">获取四点坐标</el-button>
</el-col>
</el-row>
<el-row class="mt20" v-if="cornerCoordinates.length">
<el-col :span="24">
<div class="coordinates-display">
<h3>图片四角坐标:</h3>
<el-input
type="textarea"
:rows="4"
v-model="coordinatesString"
readonly
></el-input>
<el-button type="primary" @click="copyCoordinates" class="mt10">复制坐标</el-button>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { ElInput, ElButton, ElRow, ElCol, ElUpload, ElSlider, ElCheckbox, ElMessage } from 'element-plus'
export default {
components: {
ElInput,
ElButton,
ElRow,
ElCol,
ElUpload,
ElSlider,
ElCheckbox
},
data () {
return {
map: null,
locationName: '',
opacity: 0.7,
customImage: null,
isDraggable: true,
imageWidth: 0,
imageHeight: 0,
scale: 1,
imageLeft: 0,
imageTop: 0,
isDragging: false,
dragStartX: 0,
dragStartY: 0,
mapZoom: 10,
cornerCoordinates: [],
rotation: 0,
}
},
computed: {
imageStyle() {
return {
opacity: this.opacity,
transform: `scale(``{this.scale}) rotate(``{this.rotation}deg)`,
left: `${this.imageLeft.toFixed(2)}px`,
top: `${this.imageTop.toFixed(2)}px`,
cursor: this.isDraggable ? 'move' : 'default',
transformOrigin: 'center center',
}
},
coordinatesString() {
return JSON.stringify(this.cornerCoordinates)
}
},
mounted () {
const huidongCountyCoords = [114.7205, 22.9846]
this.map = new AMap.Map(this.$refs.mapContainer, {
zoom: this.mapZoom,
center: huidongCountyCoords,
viewMode: '3D',
zooms: [1, 20],
zoomEnable: true,
expandZoomRange: true,
})
// 添加 zoomchange 事件监听器
this.map.on('zoomchange', this.onZoomChange)
window.addEventListener('mousemove', this.onDrag)
window.addEventListener('mouseup', this.stopDrag)
window.addEventListener('touchmove', this.onDrag)
window.addEventListener('touchend', this.stopDrag)
},
watch: {
rotation() {
this.updateRotation()
},
},
beforeDestroy () {
if (this.map) {
this.map.destroy()
}
window.removeEventListener('mousemove', this.onDrag)
window.removeEventListener('mouseup', this.stopDrag)
window.removeEventListener('touchmove', this.onDrag)
window.removeEventListener('touchend', this.stopDrag)
},
methods: {
// 新增 onZoomChange 方法
onZoomChange() {
this.mapZoom = parseFloat(this.map.getZoom().toFixed(3))
},
updateRotation() {
this.rotation = parseFloat(this.rotation.toFixed(2))
},
handleImageChange(file) {
const reader = new FileReader()
reader.onload = (e) => {
this.customImage = e.target.result
const img = new Image()
img.onload = () => {
this.imageWidth = img.width
this.imageHeight = img.height
this.centerImage()
}
img.src = this.customImage
}
reader.readAsDataURL(file.raw)
},
centerImage() {
const mapSize = this.map.getSize()
this.imageLeft = (mapSize.width - this.imageWidth) / 2
this.imageTop = (mapSize.height - this.imageHeight) / 2
},
startDrag(e) {
if (this.isDraggable) {
this.isDragging = true
if (e.type === 'touchstart') {
this.dragStartX = e.touches[0].clientX - this.imageLeft
this.dragStartY = e.touches[0].clientY - this.imageTop
} else {
this.dragStartX = e.clientX - this.imageLeft
this.dragStartY = e.clientY - this.imageTop
}
}
},
onDrag(e) {
if (this.isDragging) {
e.preventDefault()
let clientX, clientY
if (e.type === 'touchmove') {
clientX = e.touches[0].clientX
clientY = e.touches[0].clientY
} else {
clientX = e.clientX
clientY = e.clientY
}
this.imageLeft = parseFloat((clientX - this.dragStartX).toFixed(2))
this.imageTop = parseFloat((clientY - this.dragStartY).toFixed(2))
}
},
stopDrag() {
this.isDragging = false
},
searchLocation() {
if (!this.locationName) {
ElMessage.warning('请输入地名')
return
}
const geocoder = new AMap.Geocoder()
geocoder.getLocation(this.locationName, (status, result) => {
if (status === 'complete' && result.info === 'OK') {
const location = result.geocodes[0].location
this.map.setCenter([location.lng, location.lat])
this.map.setZoom(15)
} else {
ElMessage.error('地址查询失败')
}
})
},
updateImagePosition() {
this.imageLeft = parseFloat(this.imageLeft.toFixed(2))
this.imageTop = parseFloat(this.imageTop.toFixed(2))
},
updateMapZoom() {
this.mapZoom = parseFloat(this.mapZoom.toFixed(3))
this.map.setZoom(this.mapZoom)
},
getCornerCoordinates() {
const centerX = this.imageLeft + this.imageWidth * this.scale / 2;
const centerY = this.imageTop + this.imageHeight * this.scale / 2;
const corners = [
[-this.imageWidth * this.scale / 2, -this.imageHeight * this.scale / 2],
[this.imageWidth * this.scale / 2, -this.imageHeight * this.scale / 2],
[this.imageWidth * this.scale / 2, this.imageHeight * this.scale / 2],
[-this.imageWidth * this.scale / 2, this.imageHeight * this.scale / 2]
];
const rotatedCorners = corners.map(corner => {
const x = corner[0];
const y = corner[1];
const rotatedX = x * Math.cos(this.rotation * Math.PI / 180) - y * Math.sin(this.rotation * Math.PI / 180);
const rotatedY = x * Math.sin(this.rotation * Math.PI / 180) + y * Math.cos(this.rotation * Math.PI / 180);
return [centerX + rotatedX, centerY + rotatedY];
});
const coordinates = rotatedCorners.map(corner => {
const lnglat = this.map.containerToLngLat(new AMap.Pixel(corner[0], corner[1]));
return [lnglat.getLng(), lnglat.getLat()];
});
this.cornerCoordinates = JSON.stringify(coordinates);
},
copyCoordinates() {
navigator.clipboard.writeText(this.cornerCoordinates).then(() => {
ElMessage.success('坐标已复制到剪贴板')
}).catch(err => {
console.error('无法复制文本: ', err)
ElMessage.error('复制失败,请手动复制')
})
}
}
}
</script>
<style lang="scss" scoped>
.gdMap {
position: fixed;
width: 100%;
height: 100%;
z-index: 0;
.container {
position: relative;
width: 100%;
height: 100%;
z-index: 10;
}
}
.custom-image {
position: absolute;
z-index: 20;
pointer-events: auto;
transform-origin: top left;
user-select: none;
-webkit-user-drag: none;
}
.operation {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
border-radius: 5px;
background: rgba(0, 0, 0, 0.8);
width: 300px;
padding: 20px;
color: #fff;
h2 {
margin-top: 0;
margin-bottom: 20px;
font-size: 18px;
}
.label {
line-height: 32px;
}
}
.mt20 {
margin-top: 20px;
}
:deep(.el-input__inner) {
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
:deep(.el-button) {
width: 100%;
}
:deep(.el-upload) {
width: 100%;
}
:deep(.el-slider__runway) {
background-color: rgba(255, 255, 255, 0.3);
}
:deep(.el-slider__bar) {
background-color: #409EFF;
}
:deep(.el-slider__button) {
border-color: #409EFF;
}
</style>
接下来我们就是勾勒轮廓,获取坐标了:vue通过图片获取geoJson数据(轮廓勾勒)