vue项目通过图片获取geoJson数据(图片配准)

vue yekong

最近有一个项目,客户要求根据经纬度将一些标注渲染在图片上,但是客户没有提供geoJson数据,如果没有geoJson那么就没办法将标注渲染到图片准确的位置上。为了能够准确的将标注渲染到坐标上,我们需要根据图片来想办法获取到geoJson数据。

因为这是一个小区域的坐标范围,并不是常规的省市县范围,所以我们没有办法找现成的geoJson,只能手动获取。

vue项目通过图片获取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数据(轮廓勾勒)

喜欢