唯杰地图 logo唯杰地图
VJMAP
EN

measure

vjmap/references/interaction/measure.md 7.4 KB193 行
原文

本文回答:测量距离 / 面积 / 角度 / 坐标,完整流程与快捷键。

一、核心思路

测量全部依托 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 记下来,逐条删除。

十一、常见坑

  1. 距离计算 on 经纬度坐标:必须 map.fromLngLat 转回 CAD 再算,否则单位是度。
  2. 测量过程中切换模式 → 先发 ESC 结束当前,再切下一个。
  3. popup.setLngLat([0,0]):取消 trackPointer 的绑定。
  4. 长图大批捕捉点:conditionQueryFeature 拉 10 万条会慢;按视口范围查。
  5. calcPolygonArea 入参是 GeoPoint[](内部读 .x/.y):传 [[x,y], ...] 二维数组得 NaN 而非报错(calcPolygonArea([[0,0],[10,0],[10,10]]) = NaN)。map.fromLngLat(coords) 返回的就是 GeoPoint[],直接喂给它。
  6. GeoPoint.angleTo(p) 是反向约定:p1.angleTo(p0) = atan2(p1.y-p0.y, p1.x-p0.x)——方向是"从 p0 指向 p1"(this 是终点、参数是起点)。(0,0).angleTo((1,0)) 返回 π 不是 0。
  7. 键盘两条通道:Draw 的 keybindings 默认 true(actionDraw* 内部 Tool 也是),且 actionDraw* 默认把焦点给 canvas(canvasAutoFocus 不为 false 时),所以用户真实按 ESC / Enter / Backspace 由地图容器上的 DOM keydown/keyup 监听处理(canvas 失焦则无效;传 keybindings:false 才不挂 DOM 监听)。vjmap 不会把 DOM 按键桥接成 map keyup 事件——程序化取消/完成/删点用 map.fire("keyup", {keyCode})(文档上方写法即此通道)。
  8. 自动化点击必须先 mousemove:SnapLineMode.onClick 用的是 state.snappedLng/Lat——由 onMouseMove 写入;不发 mousemove 直接点击,snappedLng/Lat 为 undefined → onClick 静默早退(点不落)。
  9. map click 事件有点击容差门控:底层 click 处理器要求前置 mousedown(_mousedownPos),且 click 位置距 mousedown < tolerance,否则吞掉;_mousedownPos 写入后不会因后续鼠标操作清除——模拟 click 时务必补 mousedown+mouseup 同点序列。
  10. resolve 载荷含 state(环形引用):actionDraw* resolve 的 {features, state, styleId} 不能直接 JSON.stringify(state.feature 回链 ctx/map);取 features[0].geometry 即可。
  11. updatecoordinate 载荷:{lnglat:[lng,lat], feature, event, state, styleId}——e.lnglat 是当前动点渲染经纬度数组,e.feature 是 draw 内部要素(feature.coordinates 含尾随动点)。