# 坐标系桥接（EPSG / 四参数 / 经纬度）

> 文档：`vjcad/doc/tutorial/web-map-overlay.md`、`vjcad/doc/tutorial/map-overlay.md`
> 示例：`vjcad/example/basic/05mapoverlay.js`

地图集成中最容易踩的坑就是 **坐标系**。本页梳理 vjcad 集成里出现的所有坐标空间，及其相互转换方式。

## 1. 涉及的几个坐标空间

| 空间 | 例子 | 用途 |
|---|---|---|
| **CAD 内部坐标** | `[100, 200]` | vjcad 引擎绘制 / 实体属性 |
| **CAD 投影坐标** | EPSG:4509 等 CGCS2000 / 西安 80 / 北京 54 | 传统国家测绘投影 |
| **Web Mercator** | EPSG:3857 | 互联网瓦片标准 |
| **WGS84 经纬度** | EPSG:4326 | GPS、谷歌地图、天地图 |
| **GCJ02 经纬度** | "火星坐标" | 高德、腾讯（国内偏移） |
| **BD09** | 百度坐标 | 百度地图（在 GCJ02 基础上再加密） |

## 2. 转换路径全景

```mermaid
flowchart LR
    CAD[CAD 坐标 WCS] -- srs:EPSG 服务端 proj4 --> WGS84
    CAD -- fourParameterBefore 四参数 --> WebMerc[EPSG:3857]
    WebMerc -- 反算经纬度 --> WGS84
    WGS84 -- tileCrs=gcj02 时服务端加偏 --> GCJ02
    GCJ02 -- GCJ02↔BD09 需 gcoord 等 --> BD09
```

`enableWebMap` 的换算全在服务端：每块瓦片的 CAD bbox 按 `srs`（优先）或 `fourParameterBefore` 换成 WGS84，再按 `tileCrs` 抓瓦片；`mapbounds` 不参与。你要做的只是 **声明 CAD 的坐标系**（自动推断 EPSG 或手动给四参数）。

## 3. 自动模式（推荐）：知道 EPSG

```js
Engine.enableWebMap({
    tileCrs: 'gcj02',
    tileUrl: [...],
    mapbounds: 'minX,minY,maxX,maxY',   // TS 必填，不参与对齐
    srs:       'EPSG:4509'
});
```

| `srs` | 含义 |
|---|---|
| `EPSG:4326` | WGS84 经纬度（很少在 CAD 内部用） |
| `EPSG:3857` | Web Mercator |
| `EPSG:4509` | CGCS2000 6 度带，中央经线 117°E（无带号前缀，X 为 6 位） |
| `EPSG:4526` | CGCS2000 3 度带第 38 带（X 带 `38` 前缀） |
| `EPSG:4499` | CGCS2000 6 度带第 21 带（X 带 `21` 前缀） |
| `EPSG:2362` | 西安 80 3 度带第 38 带 |
| `EPSG:2415` | 北京 54 3 度带第 39 带 |

详细 EPSG 范围见 [web-map-overlay](web-map-overlay.md) 第 5 节。

## 4. 手动模式：四参数（2D Helmert）

```
WebMercator(x, y) = Rotation(rotation) · Scale(scale) · CAD(x, y) + (dx, dy)
```

```js
Engine.enableWebMap({
    /* ... */
    fourParameterBefore: 'dx,dy,scale,rotation'
    // 如：'12727039.38, 3579066.69, 1, 0'
});
```

四个值含义：

| 参数 | 含义 | 单位 |
|---|---|---|
| `dx` | X 平移 | m |
| `dy` | Y 平移 | m |
| `scale` | 缩放比例 | 倍数 |
| `rotation` | 旋转角 | **弧度** |

何时用四参数？

- 图纸用了自定义本地坐标（厂区 / 工地）
- 已知一组对应点，能算出线性变换
- 不在标准 EPSG 列表里的老坐标系

## 5. 用对应点反算四参数

vjcad 命令面板 `WEBMAPOVERLAY` 自带"对应点 → 四参数"求解器：

1. 在面板"手动 / 对应点"页签内
2. 输入 ≥ 2 组 `(CAD x, CAD y) ↔ (lng, lat)`——经纬度 **必须是 WGS84**（面板内部按 WGS84→EPSG:3857 换算）；从高德等 GCJ02 底图取的点要先转 WGS84，否则整体偏移数百米
3. 勾选"不旋转"可锁 `rotation = 0`（默认勾选）
4. 点击"计算四参数"

实现上是最小二乘求解 2D Helmert：4 个未知量，每组对应点提供 2 个方程，因此最少 2 组点（4 方程，4 未知）；推荐 4~10 组点取均匀分布的位置。

## 6. CAD 内部坐标 ↔ 经纬度

通过 `CadMapOverlay` 的内部映射：

```js
// CAD 内部坐标 → 地理经纬度（返回对象 {lng, lat}，不是数组）
const { lng, lat } = cadOverlay.cadToLngLat(cadX, cadY);

// 经纬度 → CAD 内部坐标（返回对象 {x, y}；首参可传 lng 数字或 {lng,lat} 对象）
const { x: cadX, y: cadY } = cadOverlay.lngLatToCad(lng, lat);
```

> 注意：`cadToLngLat` / `lngLatToCad` 只是转调 vjmap 的 `map.toLngLat` / `map.fromLngLat`，换算由 `map.attach(svc, prj)` 绑定的投影决定，与 overlay 的 `bounds` 无关。CAD 图在 vjmap 里通常用 `vjmap.GeoProjection(mapExtent)` 打开，得到的是把图纸范围线性铺到墨卡托上的 **伪经纬度**，不是真实地理坐标。

## 7. 与 vjmap 的 `GeoProjection` 配合

```js
const mapExtent = vjmap.GeoBounds.fromString(res.bounds);
const prj       = new vjmap.GeoProjection(mapExtent);

const lngLat = prj.toLngLat([cadX, cadY]);   // CAD 坐标 → 伪经纬度
const cad    = prj.fromLngLat([lng, lat]);   // 伪经纬度 → CAD 坐标
```

`map.attach(svc, prj)` 之后 `cadOverlay.cadToLngLat` 与 `prj.toLngLat` 是同一套换算，精度没有差别，都只用于在这张 vjmap 地图上定位。

需要 **真实经纬度**（CAD 图是标准投影坐标）时，用服务端坐标转换 `Engine.service.cmdTransform(srs, crs, points, fourParameter?, isInverseFourParamter?)`（返回 `[[x, y], ...]`）：

```js
const pts = await Engine.service.cmdTransform(
    'EPSG:4509', 'EPSG:4326',
    [new GeoPoint(cadX, cadY)]           // GeoPoint 或数组；可选第 4 参四参数 "dx,dy,scale,rotation"
);
const [lng, lat] = pts[0];               // WGS84 经纬度
```

## 8. GCJ02 / WGS84 / BD09 互转

vjcad 不内置坐标加密 / 解密；常用 `gcoord` 等库：

```js
// CDN 加载 gcoord 后
const wgs84 = gcoord.transform([lng, lat], gcoord.GCJ02, gcoord.WGS84);
const gcj02 = gcoord.transform([lng, lat], gcoord.WGS84, gcoord.GCJ02);
const bd09  = gcoord.transform([lng, lat], gcoord.GCJ02, gcoord.BD09);
```

WebMap 集成中常见组合：

| 瓦片源 | `tileCrs` | 对应点经纬度（四参数求解用） |
|---|---|---|
| 高德 | `'gcj02'` | WGS84（服务端按 WGS84→GCJ02 拉瓦片；高德拾取的点先转 WGS84） |
| 天地图 | `'wgs84'` | WGS84 |
| 谷歌（境外） | `'wgs84'` | WGS84 |
| 百度 | 不支持（`tileCrs` 只有 `'gcj02' \| 'wgs84'`） | — |

## 9. CadMapOverlay 的 `bounds`

`CadMapOverlay` 的 `bounds` 是 **CAD 坐标范围**：扁平四元组 `[minX, minY, maxX, maxY]`、带 `toArray()` 的对象（如 `vjmap.GeoBounds`）或 `{min:{x,y}, max:{x,y}}`，不是经纬度对：

```js
const cadOverlay = new CadMapOverlay({
    bounds: mapExtent,                 // 或 [minX, minY, maxX, maxY]（CAD 坐标）
    serviceUrl, accessToken
});
```

它只用来计算 CAD 视图缩放比（在范围中心取样）和 `zoomToExtents()` 的范围；CAD 与地图的对齐由 `map.attach(svc, prj)` 的投影决定。

## 10. 需要真实地理位置时

`CadMapOverlay` **没有 `srs` 选项**，叠出来的是 vjmap 伪经纬度坐标系下的位置。要让 CAD 落在真实底图（高德 / 天地图）上，用 `Engine.enableWebMap({ srs | fourParameterBefore, ... })` 在 vjcad 里铺底图；要换算单点真实经纬度用 `Engine.service.cmdTransform`（见第 7 节）。

## 红旗

- `srs` 错配：把图纸坐标系 EPSG 写成了相邻带号 → 整张图横向偏移几公里
- `tileCrs` 错配：CAD 与瓦片不在同一坐标系（GCJ02 / WGS84 弄反）→ 偏移 100~500m
- `fourParameterBefore` 的 `rotation` **是弧度**（不是度）；写成 `30` 而不是 `30 * Math.PI / 180` 会"地图歪了"
- `rotation ≠ 0` 时底图只是近似：服务端只把每块瓦片 bbox 的两个角点按四参数换算成经纬度范围，再按轴对齐矩形取图，不会旋转瓦片；建议保持 0（面板默认勾选"不旋转"）
- `CadMapOverlay` 的 `bounds` 是 CAD 坐标范围、不是经纬度，也不决定对齐；它没有 `srs`，真实地理定位走 `enableWebMap` / `cmdTransform`
- BD09 / GCJ02 / WGS84 三套坐标 **不能用减法直接转换**，必须走加密 / 解密算法（gcoord 等）
- vjcad 内部不会主动校验 EPSG 是否存在；填错的 EPSG 通常表现为"地图不显示"或"完全错位"
- 测量场景下，建议用 4 个 GCP（角点 + 中心）做对应点求四参数；2 个点容易过拟合

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/tutorial/web-map-overlay.md`、`vjcad/doc/tutorial/map-overlay.md`
- 示例：`vjcad/example/basic/05mapoverlay.js`
