measure
本文回答:测量距离 / 面积 / 角度 / 坐标,完整流程与快捷键。
一、核心思路
测量全部依托 vjmap.Draw.actionDrawLineSting / actionDrawPolygon / actionDrawPoint 完成:用户绘制 → 实时回调 updatecoordinate → 计算距离 / 面积 → 绘制完成后画保留的线 / 面 / 文字。
二、快捷键
| 键 | 动作 |
|---|---|
Alt |
临时关闭 / 开启捕捉 |
Ctrl |
临时启用正交模式(只能水平 / 垂直画) |
Backspace |
删除上一个点 |
ESC |
取消当前测量 |
Enter |
结束测量 |
右键 |
弹出上下文菜单(确认 / 取消 / 删点) |
程序内模拟:
map.fire("keyup", { keyCode: 27 }); // ESC
map.fire("keyup", { keyCode: 13 }); // Enter
map.fire("keyup", { keyCode: 8 }); // Backspace
三、测距(连续)
// 捕捉对象(需提前填 features)
const snapObj = { features: [...] };
async function measureDist() {
const line = await vjmap.Draw.actionDrawLineSting(map, {
api: { getSnapFeatures: snapObj },
updatecoordinate: (e) => {
if (!e.lnglat) return;
const co = map.fromLngLat(e.feature.coordinates.at(-1));
// 更新 popup 显示当前距离
},
contextMenu: (e) => new vjmap.ContextMenu({
event: e.event.originalEvent,
items: [
{ label: "确认", onClick: () => map.fire("keyup", { keyCode: 13 }) },
{ label: "取消", onClick: () => map.fire("keyup", { keyCode: 27 }) },
{ label: "删除上一点", onClick: () => map.fire("keyup", { keyCode: 8 }) },
{ label: "结束测距", onClick: () => map.fire("keyup", { keyCode: 27 }) }
]
})
});
if (line.cancel) return { exit: true };
// 保留线
new vjmap.Polyline({ data: line.features[0].geometry.coordinates, lineColor: "#f00", lineWidth: 2 }).addTo(map);
return {};
}
// 连续测距:测完一条自动进入下一条
async function measureDistLoop() {
while (true) {
const r = await measureDist();
if (r.exit) break;
}
}
measureDistLoop();
四、距离 / 面积 / 角度计算
// 距离(单位:米)
const dist = vjmap.Math2D.lineDist(map.fromLngLat(coordinates));
// 多边形面积(平方米)
const area = vjmap.calcPolygonArea(map.fromLngLat(coordinates));
// 两点角度(弧度)
const angle = pt0.angleTo(pt1);
const deg = vjmap.radiansToDegrees(angle);
面积单位自动降级:
let r = vjmap.calcPolygonArea(points);
let unit = "m²";
if (r >= 1e6) { r /= 1e6; unit = "km²"; }
else if (r < 1e-4) { r *= 1e4; unit = "cm²"; }
return r.toFixed(2) + " " + unit;
五、跟随鼠标的 popup
let popup = new vjmap.Popup({
closeButton: false,
closeOnClick: false
})
.setHTML("当前距离 xxx")
.setMaxWidth("500px")
.trackPointer()
.addTo(map);
.trackPointer() 让 popup 跟着鼠标走。
六、捕捉点来源
矢量样式:直接查 vjmap 的渲染要素:
snapObj.features = map.queryRenderedFeatures({ layers: ["vector-layer-lines"] });
栅格样式:不渲染矢量,向后端取坐标:
const res = await svc.conditionQueryFeature({ fields: "s3", condition: "s3 != ''", limit: 100000 });
// s3 是坐标点,格式 "x,y;x,y;..."(见 svrStyleVar 文档)
snapObj.features = res.result.map(e => {
const coords = e.s3.split(";").map(p => p.split(",").map(Number)).map(([x, y]) => map.toLngLat([x, y]));
return coords.length === 1
? { type: "Feature", geometry: { type: "Point", coordinates: coords[0] } }
: { type: "Feature", geometry: { type: "LineString", coordinates: coords } };
});
七、面积测量
const poly = await vjmap.Draw.actionDrawPolygon(map, {
api: { getSnapFeatures: snapObj },
updatecoordinate: (e) => {
const area = vjmap.calcPolygonArea(map.fromLngLat(e.feature.coordinates[0]));
// 实时显示面积
}
});
完成后以 new vjmap.Polygon({...}) 保留展示。
八、角度测量(三点)
const line = await vjmap.Draw.actionDrawLineSting(map, {
pointCount: 3, // 三个点自动结束
api: { getSnapFeatures: snapObj },
updatecoordinate: (e) => { /* 计算 angle */ }
});
const points = map.fromLngLat(line.features[0].geometry.coordinates);
const angle1 = points[0].angleTo(points[1]);
const angle2 = points[2].angleTo(points[1]);
let deg = vjmap.radiansToDegrees(angle1 - angle2);
if (deg < 0) deg = -deg;
if (deg > 180) deg = 360 - deg;
九、坐标标注
await vjmap.Draw.actionDrawPoint(map, {
updatecoordinate: (e) => {
const co = map.fromLngLat(e.lnglat);
popup.setHTML(`X: ${co.x.toFixed(2)}, Y: ${co.y.toFixed(2)}`);
}
});
十、删除测量结果
function clearAll(map) {
Object.keys(map.getStyle().sources)
.filter(e => e.indexOf("custom_") === 0)
.forEach(e => map.removeSourceEx(e));
map.removeMarkers();
}
或每次绘制完成,把 sourceId / markers 记下来,逐条删除。
十一、常见坑
- 距离计算 on 经纬度坐标:必须
map.fromLngLat转回 CAD 再算,否则单位是度。 - 测量过程中切换模式 → 先发 ESC 结束当前,再切下一个。
- popup.setLngLat([0,0]):取消
trackPointer的绑定。 - 长图大批捕捉点:
conditionQueryFeature拉 10 万条会慢;按视口范围查。 calcPolygonArea入参是GeoPoint[](内部读.x/.y):传[[x,y], ...]二维数组得NaN而非报错(calcPolygonArea([[0,0],[10,0],[10,10]])= NaN)。map.fromLngLat(coords)返回的就是 GeoPoint[],直接喂给它。GeoPoint.angleTo(p)是反向约定:p1.angleTo(p0)=atan2(p1.y-p0.y, p1.x-p0.x)——方向是"从 p0 指向 p1"(this是终点、参数是起点)。(0,0).angleTo((1,0))返回 π 不是 0。- 键盘两条通道:Draw 的
keybindings默认 true(actionDraw*内部 Tool 也是),且actionDraw*默认把焦点给 canvas(canvasAutoFocus不为 false 时),所以用户真实按 ESC / Enter / Backspace 由地图容器上的 DOM keydown/keyup 监听处理(canvas 失焦则无效;传keybindings:false才不挂 DOM 监听)。vjmap 不会把 DOM 按键桥接成 mapkeyup事件——程序化取消/完成/删点用map.fire("keyup", {keyCode})(文档上方写法即此通道)。 - 自动化点击必须先 mousemove:
SnapLineMode.onClick用的是state.snappedLng/Lat——由onMouseMove写入;不发 mousemove 直接点击,snappedLng/Lat为 undefined → onClick 静默早退(点不落)。 - map
click事件有点击容差门控:底层 click 处理器要求前置mousedown(_mousedownPos),且 click 位置距 mousedown < tolerance,否则吞掉;_mousedownPos写入后不会因后续鼠标操作清除——模拟 click 时务必补 mousedown+mouseup 同点序列。 - resolve 载荷含
state(环形引用):actionDraw*resolve 的{features, state, styleId}不能直接JSON.stringify(state.feature回链 ctx/map);取features[0].geometry即可。 updatecoordinate载荷:{lnglat:[lng,lat], feature, event, state, styleId}——e.lnglat是当前动点渲染经纬度数组,e.feature是 draw 内部要素(feature.coordinates含尾随动点)。