高德地图的测距功能很好用------画线实时显示长度,画完后标注永久留在地图上。我照着这个效果用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文件