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