唯杰地图 logo唯杰地图
VJMAP
EN

projection-api

vjmap/references/coord/projection-api.md 8.3 KB181 行
原文

本文回答:GeoProjection / LnglatProjection / map.toLngLat/fromLngLat / map.project/unproject / toMercator/fromMercator / wcsToUcs/ucsToWcs 的用法。

一、两种投影类

// CAD 底图:CAD 范围映射到 (-180..180, -85..85)
const prj = new vjmap.GeoProjection(res.bounds);     // 字符串或 GeoBounds 都可

// 互联网底图:无需 bounds,直接使用经纬度
const prj = new vjmap.LnglatProjection();

公共方法:

prj.toLngLat(cadPoint);          // CAD -> 渲染经纬度
prj.fromLngLat(lngLat);          // 渲染经纬度 -> CAD
prj.getMapExtent();              // 返回 GeoBounds(CAD 空间)

二、Map 上的等价方法

map.attach(svc, prj) 之后(没 attach 时 toLngLat 退化为恒等 [x,y] 数组、fromLngLat 返回 GeoPoint——互联网底图碰巧能用,CAD 底图直接错):

Map API Projection API 用途
map.toLngLat(cad) prj.toLngLat(cad) CAD → 渲染经纬度
map.fromLngLat(lngLat) prj.fromLngLat(lngLat) 渲染经纬度 → CAD
—(Map 上没有) prj.toMercator(cad) / prj.fromMercator(mkt) CAD ↔ 拉伸墨卡托米(±20037508,非 0~1 归一化、非真实地理墨卡托)
map.project(lngLat) — 渲染经纬度 → 屏幕像素(引擎原生)
map.projectEx(lngLat, altitude?) — 带高度的像素投影,3D 俯仰时更稳
map.unproject([x, y]) — 屏幕像素 → 渲染经纬度
map.getMapExtent() prj.getMapExtent() 当前图的 CAD 范围(GeoBounds)
map.getGeoBounds(ratio?) — ⚠️ 全图范围 × ratio 的 CAD 范围,不是当前视野、不是经纬度!ratio 默认取 initViewScale(sys_cad2000 为 0.4,缺省回退 0.7);缩放平移都不改变它。要当前视野的 CAD 范围:map.fromLngLat(map.getBounds())
map.getEnvelopBounds(wkt) — 解析查询结果的 envelop(墨卡托 WKT)→ CAD 空间 GeoBounds(内部 prj.fromMercator)
map.updateMapExtent(b) — 动态改变 CAD 范围,重建 projection
map.wcsToUcs(wcsPt) / map.ucsToWcs(ucsPt) — WCS ↔ UCS(map.hasUcs() 为 true 时有效)

长度类 API(toMeter/fromMeter 在 Projection 上,Map 上没有!调用报 map.toMeter is not a function)

API 语义 示例值(sys_cad2000)
prj.toMeter(len) / prj.fromMeter(m) CAD 长度 ↔ 拉伸后的墨卡托米 toMeter(1) = 6152.3——该图 1 图纸单位 = 1 真实米,所以这个"米"不是地面米,只是渲染空间尺度
map.geoLengthToHeight(len) CAD 长度 → 3D 拉伸高度值 内部即 getMapProjection().toMeter(len)
map.geoToMercatorLength(len) / map.mercatorToGeoLength(len) CAD 长度 ↔ 墨卡托长度 同一尺度
map.pixelToGeoLength(len, zoom) 像素 → CAD 长度 = extent.width*len/(2^zoom*512),zoom 必填;查询 simplifyTolerance 用
map.geoToPixelLength(len, zoom) CAD 长度 → 像素 反向
map.pixelToHeight(len, zoom) 像素 → 高度值 geoLengthToHeight(pixelToGeoLength(...))

要真实地面米:有 EPSG 的图图纸单位本身就是米,直接用 CAD 长度;否则 cmdTransform 转 WGS84 后算大圆距离。别用 toMeter。

三、接受的参数形态

toLngLat/fromLngLat 都支持 点对象 / 数组 / 完整 GeoJSON:

map.toLngLat([100, 200]);                           // [x, y]
map.toLngLat({ x: 100, y: 200 });                   // Point2D
map.toLngLat(vjmap.geoPoint([100, 200]));           // GeoPoint
map.toLngLat({ type: "FeatureCollection", ... });   // 整棵 GeoJSON,递归转换

这也是 vjmap.transform.convert(data, fn) 的用法:遍历所有坐标对执行回调。

⚠️ 原地修改陷阱

map.toLngLat / map.fromLngLat 只对 type==='FeatureCollection' 做 cloneDeep;单个 Feature / 裸 Geometry 会被原地改写坐标并返回同一个对象:

// ✅ 安全:FC 走 cloneDeep,原对象不变
const fc2 = map.toLngLat(fc);              // fc 仍是 CAD 坐标,fc2 是渲染经纬度

// ❌ 危险:单 Feature / 裸 Geometry 被原地改坏
const feat = { type:'Feature', properties:{}, geometry:{ type:'Point', coordinates:[cx,cy] } };
const out  = map.toLngLat(feat);           // out === feat,feat.geometry.coordinates 已变成渲染经纬度
map.toLngLat(feat);                        // 再调一次 = 二次转换,坐标彻底跑偏

// 需要保留原始 CAD 数据时自己 clone,或统一包成 FeatureCollection 再转
const out2 = map.toLngLat(JSON.parse(JSON.stringify(feat)));

vjmap.transform.convert(geojson, …) 同样原地修改;对点/点数组输入才返回新值。

cloneDeep 是 map.* 层的包装(源码 Map.prototype.toLngLat 对 FC 先 cloneDeep 再 transform.convert);底层 prj.toLngLat(GeoProjection)对 FeatureCollection 也原地改写。直接用 prj 转 GeoJSON 前先自己克隆。

GeoBounds 判定方法

const b = vjmap.GeoBounds.fromArray([0, 0, 100, 80]);  // 或 res.bounds(openMap 返回)
b.contains(new vjmap.GeoPoint(50, 40));  // ✅ true —— 收 GeoPoint 或 {x,y} 对象
b.contains([50, 40]);                    // ❌ 恒 false!数组不被识别(d.ts 只声明 GeoPoint,{x,y} 兼容)
b.intersect(other);                      // 返回相交 GeoBounds 或 null —— 不是 boolean
b.intersects;                            // ❌ 不存在(无 s)
b.width(); b.height();                   // 100 / 80
b.center();                              // GeoPoint {x:50,y:40}
b.scale(2);                              // ⚠️ 以中心为基准缩放,返回新 GeoBounds(b 不变):得 -50,-40,150,120
b.min / b.max / b.toArray();             // {x,y} / [0,0,100,80]
// 经纬度域判定点用 map.getBounds().contains(ll)(LngLatBounds,收 LngLatLike 数组)

四、常用场景代码

屏幕像素 → CAD 坐标

map.on("click", (e) => {
  const lngLat = e.lngLat;                    // 点击处的渲染经纬度
  const cadPt = map.fromLngLat(lngLat);       // 转 CAD
  console.log("CAD 坐标:", cadPt);
});

CAD 距离 → 屏幕像素

function cadLengthToPixels(cadLength) {
  const c = map.getCenter();
  const p1 = map.project(c);
  const cadCenter = map.fromLngLat(c);
  const cadOther = vjmap.geoPoint([cadCenter.x + cadLength, cadCenter.y]);
  const p2 = map.project(map.toLngLat(cadOther));
  return Math.hypot(p2.x - p1.x, p2.y - p1.y);
}

整片 GeoJSON 批量转换

const cadJson = map.fromLngLat(drawGeoJson);    // 所有坐标:渲染经纬度 → CAD
const lngLatJson = map.toLngLat(cadJson);       // 反向

五、vjmap.geoPoint 工具

规范化任意入参为 GeoPoint:

vjmap.geoPoint([100, 200]);         // 数组
vjmap.geoPoint({ x: 100, y: 200 }); // 对象
vjmap.geoPoint({ lng: 116, lat: 39 });

常与 prj/map API 组合调用。

六、动态范围

如果业务会扩大 CAD 范围(绘图、加实体),res.bounds 可能不够大。要让 projection 跟上:

// 比如 mapext 绘完一条直线后
map.updateMapExtent("[0, 0, 20000, 20000]");  // 传字符串
map.fitMapBounds();

updateMapExtent 内部会重建 GeoProjection,之后再调用 toLngLat 就会使用新范围。

七、点击/查询的坐标方向

map.on('click', e => {
  const ll  = e.lngLat;                 // 渲染经纬度(CAD 底图下无地理意义)
  const cad = map.fromLngLat(ll);       // CAD 坐标——查库/量算用它
  const px  = e.point;                  // 屏幕像素
});

pointQueryFeature({x, y, ...}) 的 x/y 是 CAD 坐标——把经纬度值当 CAD 传入点查询会得到 recordCount:0。

enableLayerClickHighlight 完整签名(源码核实,内部已做 fromLngLat → pointQueryFeature,全程无需手动转):

map.enableLayerClickHighlight(
  svc,
  cb?,            // (res, event) => void,res.result[i].geom 已是渲染经纬度
  HighlightColor?,// "#0000" 表示不绘制高亮
  prefix?,        // 高亮图层名前缀,缺省 "highlight"
  filterCb?,      // (curResult, zoom?, x?, y?) => any,x/y 是 CAD 坐标
  enterQueryCb?   // (lngLat) => IPointQueryFeatures,改查询参数的钩子(自定义坐标/pixelsize)
);