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

## 一、两种投影类

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

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

公共方法：

```js
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**：

```js
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 会被原地改写坐标并返回同一个对象**：

```js
// ✅ 安全：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 判定方法

```js
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 坐标

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

### CAD 距离 → 屏幕像素

```js
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 批量转换

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

## 五、`vjmap.geoPoint` 工具

规范化任意入参为 `GeoPoint`：

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

常与 `prj`/`map` API 组合调用。

## 六、动态范围

如果业务会扩大 CAD 范围（绘图、加实体），`res.bounds` 可能不够大。要让 projection 跟上：

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

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

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

```js
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`，全程无需手动转）：

```js
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）
);
```
