OpenLayers测距测面:完整测量工具

高德地图的测距功能很好用------画线实时显示长度,画完后标注永久留在地图上。我照着这个效果用OpenLayers实现了一遍,发现核心API很简单(getLength/getArea),但要做到"画到哪量到哪"、"标注跟随鼠标"、"历史记录管理"这些体验细节,还是有不少门道的。


先说效果

最终实现的测量工具包含:

  • 测距(LineString):实时显示长度,支持km/m自动切换
  • 测面(Polygon):实时显示面积,支持km²/m²自动切换
  • 跟随鼠标的临时标注(help tooltip)
  • 固定在地图上的永久标注(measure tooltip)
  • 测量历史记录,支持单条删除
  • 详细信息(段数、顶点数)

一、30秒跑通最简测量

核心就两个API:

javascript 复制代码
import {getLength, getArea} from 'ol/sphere'

// 测距:传入LineString
const length = getLength(lineGeometry)  // 返回米

// 测面:传入Polygon
const area = getArea(polygonGeometry)   // 返回平方米

然后格式化输出:

javascript 复制代码
const formatLength = (line) => {
  const length = getLength(line)
  return length > 1000
    ? `${(length / 1000).toFixed(3)} km`
    : `${Math.round(length)} m`
}

const formatArea = (polygon) => {
  const area = getArea(polygon)
  return area > 1000000
    ? `${(area / 1000000).toFixed(3)} km²`
    : `${Math.round(area)} m²`
}

但光有计算还不够------用户画线的时候,需要实时看到长度变化 ,画完后需要标注永久显示在地图上


二、实时测量:画到哪量到哪

2.1 核心原理

和绘制功能一样,实时测量的关键是在drawstart时监听feature的change事件:

javascript 复制代码
drawInteraction.on('drawstart', (event) => {
  const sketchFeature = event.feature

  sketchFeature.on('change', (e) => {
    const geom = e.target.getGeometry()

    if (measureType === 'distance') {
      const output = formatLength(geom)
      // 标注放在线段倒数第二个点上方
      const coords = geom.getCoordinates()
      tooltipCoord = coords[coords.length - 2]
      currentResult.value = { value: output }
    } else {
      const output = formatArea(geom)
      // 标注放在多边形内部
      tooltipCoord = geom.getInteriorPoint().getCoordinates()
      currentResult.value = { value: output }
    }

    // 更新Overlay位置
    tooltipOverlay.setPosition(tooltipCoord)
    tooltipElement.textContent = currentResult.value.value
  })
})

2.2 标注位置的选择

测量类型 标注位置 原因
测距 倒数第二个点 最后一个点跟随鼠标,放那里会闪烁
测面 多边形内部点 getInteriorPoint()保证在图形内部

:测距时如果把标注放在最后一个点(跟随鼠标的点),标注会剧烈抖动。放在倒数第二个点就稳定了。


三、Overlay标注系统

测量工具需要两种Overlay:

3.1 临时标注(help tooltip)

跟随鼠标移动,提示用户操作:

javascript 复制代码
const createHelpTooltip = () => {
  const tooltip = document.createElement('div')
  tooltip.className = 'ol-tooltip ol-tooltip-help'
  tooltip.style.cssText = `
    position: absolute;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    padding: 3px 8px;
    border-radius: 3px;
    font-size: 12px;
    white-space: nowrap;
    pointer-events: none;
    transform: translate(-50%, -120%);
  `
  const overlay = new Overlay({
    element: tooltip,
    offset: [0, 0],
    positioning: 'bottom-center',
    stopEvent: false,
  })
  map.value.addOverlay(overlay)
  return { overlay, element: tooltip }
}

3.2 永久标注(measure tooltip)

测量完成后固定在地图上:

javascript 复制代码
const addPermanentTooltip = (feature, type) => {
  const geom = feature.getGeometry()
  let coord

  if (type === 'distance') {
    coord = geom.getCoordinateAt(0.5)  // 线段中点
  } else {
    coord = geom.getInteriorPoint().getCoordinates()  // 多边形内部
  }

  const tooltip = document.createElement('div')
  tooltip.className = 'ol-tooltip ol-tooltip-static'
  tooltip.textContent = type === 'distance' ? formatLength(geom) : formatArea(geom)
  tooltip.style.cssText = `
    position: absolute;
    background: rgba(255, 71, 87, 0.85);
    color: #fff;
    padding: 3px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: bold;
    pointer-events: none;
    transform: translate(-50%, -50%);
  `

  const overlay = new Overlay({
    element: tooltip,
    position: coord,
    positioning: 'center-center',
    stopEvent: false,
  })
  map.value.addOverlay(overlay)
}

3.3 两种标注对比

临时标注 永久标注
生命周期 测量过程中 测量完成后
位置 跟随鼠标 固定在图形中心
样式 黑色半透明 红色半透明
用途 提示操作 显示结果

四、测量样式

4.1 虚线样式

测量线用虚线,和普通绘制区分:

javascript 复制代码
new Style({
  stroke: new Stroke({
    color: 'rgba(255, 71, 87, 0.9)',
    width: 3,
    lineDash: [10, 8]  // 虚线:10px实线 + 8px间隔
  })
})

4.2 顶点标记

每个顶点画一个白点红边的圆:

javascript 复制代码
// 遍历顶点,跳过最后一个(跟随鼠标的点)
coords.forEach((coord, i) => {
  if (i < coords.length - 1) {
    styles.push(new Style({
      geometry: new Point(coord),
      image: new CircleStyle({
        radius: 5,
        fill: new Fill({ color: '#fff' }),
        stroke: new Stroke({ color: '#ff4757', width: 2 })
      })
    }))
  }
})

为什么跳过最后一个点? 因为最后一个点跟随鼠标移动,显示它会导致视觉混乱。


五、详细信息计算

测量完成后,可以计算更详细的信息:

javascript 复制代码
const calculateDetail = (feature) => {
  const geom = feature.getGeometry()
  const type = geom.getType()

  if (type === 'LineString') {
    const coords = geom.getCoordinates()
    let totalSegments = coords.length - 1
    return `共 ${coords.length} 个点, ${totalSegments} 段`
  }

  if (type === 'Polygon') {
    const coords = geom.getCoordinates()[0]
    return `共 ${coords.length - 1} 个顶点`
  }

  return ''
}

六、测量历史管理

6.1 存储测量结果

javascript 复制代码
drawInteraction.on('drawend', (event) => {
  const feature = event.feature
  const geom = feature.getGeometry()

  measurements.value.push({
    type: type,
    value: type === 'distance' ? formatLength(geom) : formatArea(geom),
    detail: calculateDetail(feature),
    feature: feature.clone(),  // 克隆保存
    timestamp: new Date().toISOString()
  })
})

6.2 单条删除

通过坐标匹配找到对应的feature并移除:

javascript 复制代码
const removeMeasurement = (index) => {
  const item = measurements.value[index]
  if (item && item.feature) {
    const features = measureSource.getFeatures()
    for (const f of features) {
      if (f.getGeometry().getType() === item.feature.getGeometry().getType()) {
        const fCoords = JSON.stringify(f.getGeometry().getCoordinates())
        const itemCoords = JSON.stringify(item.feature.getGeometry().getCoordinates())
        if (fCoords === itemCoords) {
          measureSource.removeFeature(f)
          break
        }
      }
    }
  }
  measurements.value.splice(index, 1)
}

七、Overlay清理

测量完成或取消时,必须清理临时Overlay:

javascript 复制代码
const cancelMeasure = () => {
  if (drawInteraction) {
    drawInteraction.abortDrawing()
    map.value.removeInteraction(drawInteraction)
    drawInteraction = null
  }
  measuring.value = false
  currentResult.value = null
  map.value.un('pointermove', pointerMoveHandler)
  clearOverlays(measureOverlays)
  measureOverlays = []
}

const clearOverlays = (overlays) => {
  overlays.forEach(o => map.value.removeOverlay(o))
}

注意:Overlay不清理会导致内存泄漏,而且地图上会残留看不见的DOM元素。


八、踩坑记录

坑1:测距标注抖动

现象:标注在鼠标附近剧烈跳动。

原因:标注放在了最后一个点(跟随鼠标的点)。

解决 :放在倒数第二个点:coords[coords.length - 2]

坑2:测量完成后Overlay残留

现象:取消测量后,地图上还有标注。

原因 :没有清理measureOverlays数组中的Overlay。

解决cancelMeasure中调用clearOverlays(measureOverlays)

坑3:重复测量时旧Draw交互残留

现象:点了两次"测距",能同时画两条线。

原因:没有先移除旧的Draw交互。

解决startMeasure开始时先调用cancelMeasure()


九、总结

功能 核心API 关键点
测距 getLength(line) 返回米,需格式化
测面 getArea(polygon) 返回平方米,需格式化
实时测量 feature.on('change') drawstart时绑定
跟随标注 Overlay + pointermove 鼠标移动更新位置
永久标注 Overlay + setPosition 测量完成后固定
顶点标记 Style + geometry: Point 跳过最后一个点
历史管理 measurements数组 clone保存feature

更新日期 :2026年8月

调试版本 :OpenLayers 10.9.0

完整源码 :参考项目中 5.example+测距测面.vue 文件

相关推荐
zhanghaha13141 小时前
HTML系列教程:2_HTML 编辑器(零基础超详细讲解
前端·编辑器·html
半旧5181 小时前
【Java Web02】WEB服务器内部实现技术揭秘
java·服务器·前端
拿本唠嗑AI研究1 小时前
现代前端架构爬虫指南:从静态页面到 React/Vue 单页应用
前端·爬虫·架构
独孤九剑打醒他1 小时前
【原创开源】双层Master‑Worker软硬协同调度架构:从根源解决分布式数据一致性难题
前端·嵌入式硬件·架构·前端框架·硬件工程
程序员黑豆7 小时前
Java包装类:基本类型与对象的桥梁
java·前端·ai编程
bugcome_com9 小时前
JWT 知识小课堂:从入门到精通,一文吃透 JSON Web Token
前端·json
To_OC10 小时前
写了三天 Next.js,我的 React 世界观被拆了又装回去
前端·全栈·next.js
独泪了无痕10 小时前
viewer.js 安装与配置指南:实现图片预览功能
前端·vue.js
起床学FPGA12 小时前
AI回答问题之后,会自动跳到最下面的问题
开发语言·javascript·ecmascript