唯杰地图 logo唯杰地图
VJMAP
EN

cad-mapper

vjmapext/references/coord/cad-mapper.md 9.2 KB166 行
原文

本文回答:mapext 内置的 CoordMapper 类(cad.coordMapper)的所有方法与适用场景。

一、谁在用它

在 vjmapext MapCadLayer 里,所有命令、预览层、渲染管线都通过 this.coordMapper 做 CAD ↔ 渲染经纬度 / 像素的转换。外部可以通过:

const cad = new MapCadLayer();
map.addControl(cad);
const cm = cad.coordMapper;   // 绑定到 map 后才能用

它实际上是对 prj.toLngLat / fromLngLat / toMercator / fromMercator(经 map.getMapProjection() 委托)与 map.project / unproject 的薄封装,并补了几个 mapext 业务常用的便捷方法。

一之二、核心规则:实体永远存"投影源空间"坐标

MapCadLayer 实体(LineEnt/CircleEnt/TextEnt…)内部坐标 = 宿主 map 投影的源坐标:

宿主 map 投影 实体坐标 = 典型场景
GeoProjection(cad图bounds) CAD 图纸坐标(米/毫米) CAD 底图(默认玩法)
GeoProjection(EARTH_BOUNDS)(GeoBounds.fromArray([-20037508,-20037508,20037508,20037508])) Web 墨卡托米(EPSG:3857) 互联网底图直连模式——没有 CAD 图、拿瓦片当底图让 mapext 直接画
LnglatProjection 经纬度(不推荐画图) 简单撒点

渲染链路:entity.toGeoJSON()(实体坐标)→ RenderCache 逐点 coordMapper.cadToLngLat → 渲染经纬度 → geojson source。所以实体构造参数一律给"源空间"坐标,不要预先转经纬度。

⚠️ 实体类都在全局 vjmapext 命名空间下(没有 vjmap.ext),构造多为位置参数而非配置对象(以 dist/index.d.ts 为准):new vjmapext.LineEnt(startPt, endPt)、new vjmapext.CircleEnt(centerPt, radius)。MapCadLayer 的 createUI 是实例方法不是构造选项(构造选项:locale/mode/defaultColor/shortcuts/...)。实体颜色是 0xRRGGBB 整数(number),不是 CSS 字符串,也不是 CAD/ACI 色号(color = 1 会渲染成近黑 #000001)。

互联网底图直连模式下喂数据:WGS84 点要 vjmap.Projection.lngLat2Mercator 转成墨卡托米再当实体坐标;误传经纬度会得到 [0.001, 0.0004] 之类的值(漂到几内亚湾,漂移上万公里)。该模式实体"单位"是 EPSG:3857 投影米,高纬度有变形,不是真实地面距离。

二、API 一览

class CoordMapper {
  bindMap(map: Map, onExtentChanged?: () => void): void;
  unbindMap(): void;   // ⚠️ unbind 后 map=null,cadToMercator→projection undefined→
                      // 抛 "reading 'toMercator'";且 fx 渲染器共享同一实例,
                      // unbind 后所有 fx.addPoint 全挂——用完须 bindMap 回绑

  // CAD <-> 渲染经纬度
  cadToLngLat(x: number, y: number): [number, number];
  lngLatToCad(lng: number, lat: number): Point2D;
  cadPointToLngLat(pt: Point2D): [number, number];

  // CAD <-> 墨卡托
  cadToMercator(x: number, y: number): [number, number];
  mercatorToCad(mx: number, my: number): [number, number];

  // 墨卡托 <-> 经纬度(自带实现,独立于 map)
  mercatorToLngLat(mx: number, my: number): [number, number];
  lngLatToMercator(lng: number, lat: number): [number, number];

  // 便捷
  getCadCenter(): Point2D;
  getMapCenter(): [number, number];

  // 屏幕像素 <-> CAD 距离
  pixelsToCadDistance(pixels: number, map: Map): number;
  pixelOffsetToCadOffset(dxPx: number, dyPx: number, map: Map): Point2D;
}

三、典型用法

屏幕鼠标 -> CAD

map.on("mousemove", (e) => {
  const cadPt = cad.coordMapper.lngLatToCad(e.lngLat.lng, e.lngLat.lat);
  document.getElementById("coord").textContent = `${cadPt.x.toFixed(2)}, ${cadPt.y.toFixed(2)}`;
});

让 Marker 对准 CAD 点

const [lng, lat] = cad.coordMapper.cadToLngLat(300.5, 120.0);
new vjmap.Marker().setLngLat([lng, lat]).addTo(map);

像素距离 → CAD 距离(像素自适应缩放)

// 保证 fx 光圈在屏幕上永远 40 px,无论如何缩放
function pxToCad(px) {
  return cad.coordMapper.pixelsToCadDistance(px, map);
}

绘图预览(内部)

// 每帧把鼠标落点 + 实体的 CAD 几何 合成 rubber band
cad.coordMapper.cadToLngLat(fromX, fromY);
cad.coordMapper.cadToLngLat(toX, toY);

四、与 map.toLngLat/fromLngLat 的关系

在"关联了 prj 的 Map"上,coordMapper.cadToLngLat 与 map.toLngLat 底层走同一 prj.toLngLat,两者对点输入都返回 [lng,lat] 数组(源码核实:map.toLngLat(pt) → prj.toLngLat → mercator2LngLat → [number,number])。反过来是 map.fromLngLat(pt) 返回 GeoPoint(.x/.y)。写入口选择:

需要 用
[lng,lat] 给 Marker/GeoJSON map.toLngLat(cadPt) 或 cad.coordMapper.cadToLngLat(x, y)(等价)
GeoPoint/GeoBounds 结果(fromLngLat 返回 GeoPoint、LngLatBounds/GeoBounds 输入返回对应 Bounds 类型) map.fromLngLat(...) / map.toLngLat(bounds)
快速取中心点 cad.coordMapper.getCadCenter() / getMapCenter()

五、bindMap / unbindMap

map.addControl(mapcadLayer) 触发 onAdd 时自动调用 bindMap;map.removeControl 时调 unbindMap。一般不需手动。

  • bindMap(map, onExtentChanged) 会监听 updateMapExtent 事件(由业务或 map.updateMapExtent 触发)。
  • 当图范围变了(例如绘图过程中超出了原本 res.bounds),onExtentChanged 回调里需要重建缓存(如渲染 buckets 的投影坐标)。

六、查询结果 → 实体管线(queryMapEntities / featuresToEntities)

const fc = await cad.queryMapEntities({           // 或 vjmapext.queryMapEntities(map, opts)
  entTypes: ['AcDbLine','AcDbPolyline'],
  condition: "layerindex=3",
  toMapCoordinate: true,                           // 默认 true:FC 的 geometry 是 CAD 坐标
});
const ents = cad.featuresToEntities(fc);   // 实例方法已自动注入 this.blocks
// 等价:vjmapext.featuresToEntities(fc, { blocks: cad.blocks })
ents.forEach(e => cad.addEntity(e));
  • toMapCoordinate:true(默认)→ isMapCoordinate:true、每条 .geom 不生成、geojson 字段是 CAD 坐标字符串——这套 FC 正是 featuresToEntities 要的。改 toMapCoordinate:false 拿到的是墨卡托/渲染经纬度,喂进去实体全错位。
  • featureToEntity 按 properties.name(AcDb* 可读名)分发;块引用要有 blocks。
  • 坐标字段回退链:geom.geometries → points/location/center/insertionPoint/position 字符串(CAD 坐标;各实体按类型取用,如 Line 用 points/location、Circle/Arc 用 center、Text 用 location/insertionPoint/position)。
  • 类型名→SQL ID:name='AcDbText' 查不到数据,要用 entTypes 或 buildEntTypeCondition。
  • 这套 FC 要画到 vjmap 原生图层:map.toLngLat(fc) 整体转渲染经纬度再 addSource。字段空间的完整对照表见 query-coords。

六之二、fx 图层的坐标字段(FxCoord)

fx(GPU 特效)输入点支持三种写法,传哪个就写哪个键,别混:

{ cad: [x, y] }        // CAD/实体坐标——需 MapCadLayer 已绑定(CoordMapper)
{ lngLat: [lng,lat] }  // 渲染经纬度
{ mercator: [mx,my] }  // EPSG:3857 米

内部统一转归一化墨卡托;cad 模式在没绑 CoordMapper 时会抛错。

七、叠加 vjmap 原生覆盖物

mapext 不管 vjmap 原生 Marker/Symbol——它们仍按 vjmap 规则吃渲染经纬度(详见 overlay-coords):

marker.setLngLat(cad.coordMapper.cadToLngLat(x, y));        // ✅
marker.setLngLat([x, y]);                                  // ❌(除非互联网底图且 x,y 已是经纬度)

八、常见坑

  1. MapCadLayer 还没 addControl 就用 cad.coordMapper → 拿到的是 null 引用,抛 TypeError。
  2. 外部手动 map.updateMapExtent 却没触发 mapcadLayer 的重建 → 见 trouble/bounds-parse。
  3. 误把 cadToLngLat 结果写到 Point2D → 它返回数组;如要 Point2D 用 new Point2D(lng, lat)。
  4. 并行 pixelsToCadDistance 在 zoom 动画中调用 → 读到过渡中的 zoom,值会抖动;请在 moveend 后或使用 requestAnimationFrame 节流。
  5. 实体构造传渲染经纬度 → 实体存"投影源空间"坐标,CAD 底图=图纸坐标、互联网底图=3857 米;预转经纬度会画到图外。
  6. 互联网底图模式实体传经纬度 → 应 lngLat2Mercator(高德底图先转 GCJ02);误传经纬度会得到 [0.001,0.0004](几内亚湾)。
  7. featuresToEntities 喂 toMapCoordinate:false 的 FC → 墨卡托当 CAD,实体全错位。
  8. mercatorToLngLat 当地图投影用 → 纯数学 3857→WGS84,不含 CAD 映射。
  9. new vjmap.ext.XxxEnt / XxxEnt({points:...}) → vjmap.ext 不存在,实体类在 vjmapext 下且构造是位置参数。