前言

开发GIS系统,我们都会提供基本的一些测量功能给用户使用,用于测量地图上的距离和面积,这里我们使用openlayers实现此功能。

实现

1. 功能描述

用户通过在地图上多次点击确定要测量的路线或区域,点击过程中会更新现有的距离和面积,并且距离会按段显示。双击后结束单次的测量。

2. 前置知识

要实现此功能我们需要了解openlayers的一些交互组件,像我们在地图上点击画路线和区域就需要draw interaction.要在地图上显示其距离和面积需要通过overlay 显示到地图上。
我们并没有直接通过geometry对象的getLength和getArea获取长度和面积,而是通过sphere获取。如果直接调用geometry的方法获取的长度是直接通过平面坐标计算,不会考虑地球的曲率,如果本身就是平面的需求可以直接调用

3.代码实现

/*
*   地图测量工具类
* */

import {Vector as VectorLayer} from "ol/layer"
import {Vector as VectorSource} from "ol/source"
import {Circle, Fill, Stroke, Style} from 'ol/style'
import {LineString, MultiPoint, Point, Polygon} from 'ol/geom'
import {Draw} from 'ol/interaction'
import Overlay from "ol/Overlay"
import {areaFormatterEnglish, distanceFormatterEnglish, randomString} from "@/utils/util"
import Vue from 'vue'
import * as olSphere from "ol/sphere.js"


class MeasureTool {

  static LayerName = 'gis-map-measure-layer'

  constructor(olMap,options) {
    this.olMap = olMap
    this.source = new VectorSource()
    const style = MeasureTool.getStyle()
    this.layer = new VectorLayer({
      source:this.source,
      style:style,
      zIndex:100
    })
    this.olMap.addLayers({[MeasureTool.LayerName]:this.layer})
    this.interaction = null
    this.calcType = options.calcType ? options.calcType : 'sphere'
    this.featureOverlayCaches = {}
  }

  static getStyle(drawing=false){
    return [
      new Style({
        fill: new Fill({
          color: 'rgba(60, 225, 223, 0.2)'
        }),
        stroke: new Stroke({
          color: '#3acecc',
          width: 2,
          lineDash:drawing ? [3,3]:null
        }),
        image: new Circle({
          radius: 4,
          stroke: new Stroke({
            color: '#4b4b4b',
            width: 2
          }),
          fill: new Fill({
            color: '#3acecc'
          })
        })
      }),
      new Style({
        image: new Circle({
          radius: 4,
          stroke: new Stroke({
            color: '#4b4b4b',
            width: 2
          }),
          fill: new Fill({
            color: '#3acecc'
          })
        }),
        geometry: function (feature) {
          let coordinates;
          let geom = feature.getGeometry();
          if (geom instanceof Polygon) {
            coordinates = geom.getCoordinates()[0];
          } else if (geom instanceof LineString) {
            coordinates = geom.getCoordinates();
          } else if (geom instanceof Point) {
            return null;
          }
          return new MultiPoint(coordinates);
        }
      })
    ]
  }

  /**
   *  测量距离
   * */
  measureDistance() {
    this.startMeasure('LineString')
  }

  /**
   *  测量面积
   * */
  measureArea() {
    this.startMeasure('Polygon')
  }

  /**
   *  开始测量,添加interaction
   *  @param {string} type 测量类型
   * */
  startMeasure(type) {
    //显示图层
    this.layer.setVisible(true)
    this.resetMeasure()
    this.interaction = new Draw({
      source: this.source,
      type: type,
      style: MeasureTool.getStyle(true)
    })
    this.olMap.map.addInteraction(this.interaction)
    this.interaction.on('drawstart',this.drawStartHandler.bind(this))
    this.interaction.on('drawend',this.drawEndHandler.bind(this))
    //创建tooltip element
  }

  /**
   *  交互开始处理
   * */
  drawStartHandler(e){
    const feature = e.feature
    feature.setId(randomString(8))
    this.featureOverlayCaches[feature.getId()] = []
    feature.on('change',this.featureChangeHandler.bind(this))
  }

  /**
   *  交互过程中测量变化处理
   * */
  featureChangeHandler(e){
    const feature = e.target
    const geom = feature.getGeometry()
    //更新overlay
    const overlays = this.featureOverlayCaches[feature.getId()]
    if (geom instanceof LineString){
      const points = geom.getCoordinates()
      for(let i=0;i<points.length;i++){
        let overlay = overlays[i]
        if (overlay==null){
          if (i === 0){
            //起点
            overlay = this.newLabelOverlay('start',points[i],feature)
            this.olMap.map.addOverlay(overlay)
            this.featureOverlayCaches[feature.getId()][i]=overlay
          }else if (i===points.length-1 && i>0){
            //添加总距离overlay
            overlay = this.newLabelOverlay('end',points[i],feature,this.getLineStringLen(geom))
            this.olMap.map.addOverlay(overlay)
            this.featureOverlayCaches[feature.getId()][i]=overlay
            if (points.length>2){
              //更新上一个点的overlay 为截止上个点的长度
              this.olMap.map.removeOverlay(overlays[i-1])
              const segLine = new LineString(points.slice(0,i))
              const segOverlay = this.newLabelOverlay('mid',points[i-1],feature,this.getLineStringLen(segLine))
              this.olMap.map.addOverlay(segOverlay)
              this.featureOverlayCaches[feature.getId()][i-1] = segOverlay
            }
          }
        }else if (i===points.length-1){
          //更新位置
          overlay.setPosition(geom.getLastCoordinate())
          //更新内容
          overlay.setElement(this.buildDistanceOverlayTipElement('end',feature,this.getLineStringLen(geom)))
        }
      }
    }else if (geom instanceof Polygon){
      const coords = geom.getCoordinates()
        console.log('polygon coords:',coords)
      if (overlays.length ===0){
        const overlay = this.newLabelOverlay('mid',coords[0].slice(-2,-1)[0],feature,this.getPolygonArea(geom))
        overlays[0] = overlay
        this.olMap.map.addOverlay(overlay)
      }else{
        overlays[0].setPosition(coords[0].slice(-2,-1)[0])
        //更新内容
        overlays[0].setElement(this.buildAreaOverlayTipElement('mid',feature,this.getPolygonArea(geom)))
      }
    }
  }

  getLineStringLen(geom){
    if (this.calcType === 'sphere'){
      return olSphere.getLength(geom,{projection:this.olMap.getProjection()})
    }else{
      return geom.getLength()
    }
  }

  getPolygonArea(geom){
    if (this.calcType === 'sphere'){
      return olSphere.getArea(geom,{projection:this.olMap.getProjection()})
    }else{
      return geom.getArea()
    }
  }

  buildDistanceOverlayTipElement(type,feature,val){
    const containerStyle = {
      padding:'3px 6px 5px',
      display:'block',
      backgroundColor:'#4a4a4a',
      borderRadius:'4px',
      boxShadow:'0 2px 4px 0 rgba(0,0,0,0.15)'
    }
    const that = this
    const formatVal = distanceFormatterEnglish(val)
    const obj = new Vue({
      render(createElement) {
        if (type === 'start'){
          return (
            <div style={containerStyle} className="ol-tooltip ol-tooltip-static">
              <span>起点</span>
            </div>
          )
        }else if (type === 'end'){
          return (
            <div className="ol-tooltip ol-tooltip-static" style="display:flex;align-items:center">
              <div style={containerStyle}>
                <span>总距离:</span>
                <span style="color:#1890FF">{formatVal.val}</span>
                <span style="margin-left:3px">{formatVal.unit}</span>
              </div>
              <div style={Object.assign({marginLeft:'6px'},containerStyle)} onClick={()=>{that.clearMeasureByKey(feature.getId())}}>
                <a-icon type="close"/>
              </div>
            </div>

          )
        } else if (type === 'mid') {
          return (
            <div style={containerStyle} className="ol-tooltip ol-tooltip-static">
              <span>{formatVal.val}</span>
              <span style="margin-left:3px">{formatVal.unit}</span>
            </div>
          )
        }
      },
      methods:{
        clearMeasureByKey(key){
          that.clearMeasureByKey(key)
        }
      }
    }).$mount()
    return obj.$el
  }

  buildAreaOverlayTipElement(type,feature,val){
    const containerStyle = {
      padding:'3px 6px 5px',
      display:'block',
      backgroundColor:'#4a4a4a',
      borderRadius:'4px',
      boxShadow:'0 2px 4px 0 rgba(0,0,0,0.15)'
    }
    const that = this
    const formatVal = areaFormatterEnglish(val)
    const obj = new Vue({
      render(createElement) {
        if (type === 'end'){
          return (
            <div className="ol-tooltip ol-tooltip-static" style="display:flex;align-items:center">
              <div style={containerStyle}>
                <span>总面积:</span>
                <span style="color:#1890FF">{formatVal.val}</span>
                <span style="margin-left:3px">{formatVal.unit}</span>
              </div>
              <div style={Object.assign({marginLeft:'6px'},containerStyle)} onClick={()=>{that.clearMeasureByKey(feature.getId())}}>
                <a-icon type="close"/>
              </div>
            </div>
          )
        } else if (type === 'mid') {
          return (
            <div style={containerStyle} className="ol-tooltip ol-tooltip-static">
              <div>
                <span>总面积:</span>
                <span style="color:#1890FF">{formatVal.val}</span>
                <span style="margin-left:3px">{formatVal.unit}</span>
              </div>
              <div>
                <span>单击添加点,双击结束</span>
              </div>
            </div>
          )
        }
      }
    }).$mount()
    return obj.$el
  }

  /**
   *  生成提示的 overlay
   * */
  newLabelOverlay(type, position, feature, val) {
    const geom = feature.getGeometry()
    let element = null
    let yOffset = -36
    if (geom instanceof LineString){
      element = this.buildDistanceOverlayTipElement(type, feature, val)
    }else{
      element = this.buildAreaOverlayTipElement(type, feature, val)
      yOffset = -58
    }
    const overlay = new Overlay({
      element: element,
      offset: [0, yOffset],
      positioning: 'top-center'
    })
    overlay.setPosition(position)
    return overlay
  }

  /**
   *  交互完成处理
   * */
  drawEndHandler(e){
    const feature = e.feature
    const overlays = this.featureOverlayCaches[feature.getId()]
    if (feature.getGeometry() instanceof Polygon){
      if (overlays[0]){
        overlays[0].setPosition(feature.getGeometry().getLastCoordinate())
        overlays[0].setElement(this.buildAreaOverlayTipElement('end',feature,this.getPolygonArea(feature.getGeometry())))
        overlays[0].setOffset([0,-36])
      }
    }
  }


  /**
   *  重置测量 interaction
   * */
  resetMeasure() {
    if (this.interaction){
      this.olMap.map.removeInteraction(this.interaction)
      this.interaction = null
    }
  }

  /**
   *  清空所有测量信息.但不清除交互
   * */
  clearMeasure() {
    this.source.clear()
    //清除overlay
    for (let key in this.featureOverlayCaches){
      const overlays = this.featureOverlayCaches[key]
      for (let i=0;i<overlays.length;i++){
        this.olMap.map.removeOverlay(overlays[i])
      }
    }
  }

  clearMeasureByKey(key){
    this.source.removeFeature(this.source.getFeatureById(key))
    const overlays = this.featureOverlayCaches[key]
    for (let i=0;i<overlays.length;i++){
      this.olMap.map.removeOverlay(overlays[i])
    }
  }

  /**
   *  停止测量,清除interaction.清空测量信息.隐藏图层
   * */
  stopMeasure(){
    this.clearMeasure()
    this.resetMeasure()
    this.layer.setVisible(false)
  }

  destroy() {
    //移除图层
    this.olMap.removeLayer(MeasureTool.LayerName)
    //移除交互
    if (this.interactionId){
      this.olMap.map.removeInteraction(this.interactionId)
    }
    //移除element
  }

}

export default MeasureTool

效果

openlayers实现测量距离和面积

posted on 2024-06-19 14:33  猿来就是尔  阅读(4)  评论(0)    收藏  举报  来源