> **本文回答**：测量距离 / 面积 / 角度 / 坐标，完整流程与快捷键。

## 一、核心思路

测量全部依托 `vjmap.Draw.actionDrawLineSting / actionDrawPolygon / actionDrawPoint` 完成：用户绘制 → 实时回调 `updatecoordinate` → 计算距离 / 面积 → 绘制完成后画保留的线 / 面 / 文字。

## 二、快捷键

| 键 | 动作 |
|---|---|
| `Alt` | 临时关闭 / 开启捕捉 |
| `Ctrl` | 临时启用正交模式（只能水平 / 垂直画） |
| `Backspace` | 删除上一个点 |
| `ESC` | 取消当前测量 |
| `Enter` | 结束测量 |
| `右键` | 弹出上下文菜单（确认 / 取消 / 删点） |

程序内模拟：

```js
map.fire("keyup", { keyCode: 27 }); // ESC
map.fire("keyup", { keyCode: 13 }); // Enter
map.fire("keyup", { keyCode: 8  }); // Backspace
```

## 三、测距（连续）

```js
// 捕捉对象（需提前填 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();
```

## 四、距离 / 面积 / 角度计算

```js
// 距离（单位：米）
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);
```

面积单位自动降级：

```js
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

```js
let popup = new vjmap.Popup({
  closeButton: false,
  closeOnClick: false
})
.setHTML("当前距离 xxx")
.setMaxWidth("500px")
.trackPointer()
.addTo(map);
```

`.trackPointer()` 让 popup 跟着鼠标走。

## 六、捕捉点来源

**矢量样式**：直接查 vjmap 的渲染要素：

```js
snapObj.features = map.queryRenderedFeatures({ layers: ["vector-layer-lines"] });
```

**栅格样式**：不渲染矢量，向后端取坐标：

```js
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 } };
});
```

## 七、面积测量

```js
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({...})` 保留展示。

## 八、角度测量（三点）

```js
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;
```

## 九、坐标标注

```js
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)}`);
  }
});
```

## 十、删除测量结果

```js
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` 含尾随动点）。
