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 文件

相关推荐
子兮曰5 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰5 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
A黄俊辉A5 天前
uniapp webview中实现 app和内嵌的H5双向通信
vue.js·json
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄5 天前
JavaScript 隐式全局变量解析
javascript
honkun65 天前
vue 表格组件 vxe-table 配置 ajax 请求自动加载数据与表单查询
vue.js·vxe-table
kybs19915 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net