# 地图叠加（CadMapOverlay）

> 文档：`vjcad/doc/ui/map-overlay.md`
> 示例：`vjcad/example/basic/05mapoverlay.js`

> 提示：`map-integration/` 完整子文档见同名目录。本页只聚焦 UI 集成层。

## 1. 用途

`CadMapOverlay` 是把 **vjcad 视图作为透明图层** 叠加到 [vjmap](https://vjmap.com) 地图上的组件。⚠️ 只支持 **`vjmap.Map` 且已 `map.attach(svc, prj)`**：坐标换算与视图同步全靠 `map.toLngLat / map.fromLngLat`，非 vjmap 的地图对象没有这两个方法（只会 `console.warn` 并把坐标原样返回）。常见场景：

- 在卫星 / 矢量地图上叠加图纸
- 联动选择：图上点选 = 地图弹层
- 在地理坐标和 CAD 内部坐标间转换
- 用 vjmap 的图层 / 标记 / 路径分析能力

## 2. 最小示例

```js
import { CadMapOverlay, DrawingManagerService } from 'vjcad';
// 前提：vjmap 地图已按官方示例初始化——
//   const res = await svc.openMap({ mapid, mapopenway: vjmap.MapOpenWay.GeomRender, style: vjmap.openMapDarkStyle() });
//   const mapExtent = vjmap.GeoBounds.fromString(res.bounds);   // CAD 坐标范围
//   const prj = new vjmap.GeoProjection(mapExtent);
//   const map = new vjmap.Map({ container: 'map', style: svc.vectorStyle(), center: prj.toLngLat(mapExtent.center()), zoom: 1 });
//   map.attach(svc, prj); await map.onLoad();

const cadOverlay = new CadMapOverlay({
    bounds: mapExtent,                    // CadBoundsLike：图纸的 CAD 坐标范围（不是经纬度）
    serviceUrl: 'http://127.0.0.1:27660/api/v1',
    accessToken: 'your-token',
    themeMode: 0,                         // 0 深色，1 浅色
    enableSelection: true,
    onSelectionChanged: entities => {     // 实参就是 EntityBase[]，不是 {entities}
        console.log('选中:', entities.length, '个');
    }
});

await cadOverlay.addTo(map);              // 返回 Promise<this>；此时才创建并初始化内部 MainView

// 要显示服务端图纸：必须在 addTo 之后，用 overlay 自己的 MainView 打开（overlay 本身没有 loadMap）
const drawingManager = new DrawingManagerService();
const r = await drawingManager.openDrawing({ type: 'imports', mapid: 'demo.dwg', version: 'v1' });
if (r.success) {
    const f = new File([r.webcadJson], 'drawing.webcad', { type: 'application/json' });
    await cadOverlay.getCadView().openDbDoc(f, r.webcadData);   // 完整流程见 file/server.md
}
cadOverlay.zoomToExtents();               // = map.fitBounds(cadBounds, {padding:50, duration:1000})
```

`addTo` 内部创建一个 `viewMode:'viewer'` 的 MainView（`backgroundAlpha:0`、`disablePointerEvents:true`、`showCrosshair:false`），渲染的是 **Engine 当前文档**——图纸打开要单独走 `DrawingManagerService.openDrawing` + `openDbDoc`（见 [file-operations](../autodraw/file-operations.md)），且必须在 `addTo` 之后（之前 overlay 的 MainView 还不存在，`Engine.view` 不是它），`CadMapOverlay` **没有 `loadMap` / `closeMap`**。

地图 ↔ CAD 坐标换算直接调用 vjmap 的 `map.toLngLat / map.fromLngLat`（由 `map.attach(svc, prj)` 的投影决定），不是 overlay 按 `bounds` 自己做线性插值；`bounds` 是 CAD 坐标范围，用于 `zoomToExtents`/`getCadBounds` 等，应与 vjmap 打开该图得到的 `res.bounds` 一致。

## 3. CadMapOverlayOptions 关键字段

```ts
interface CadMapOverlayOptions {           // d.ts 字段，仅以下这些
    bounds: CadBoundsLike;                 // 图纸的 CAD 坐标范围（如 vjmap.GeoBounds.fromString(res.bounds)）
    serviceUrl: string;                    // WebCAD 服务地址
    accessToken: string;
    themeMode?: number;                    // 0 深色（默认），1 浅色
    themeColors?: ThemeColorsPartial;      // UI chrome 主题色覆盖
    minZoom?: number;                      // 最小显示级别（默认 0）
    maxZoom?: number;                      // 最大显示级别（默认 100）
    enableSelection?: boolean;             // 允许点选实体（默认 true）
    onSelectionChanged?: (selection: EntityBase[]) => void;
    smoothAnimation?: boolean;             // true（默认）动画中 CSS transform，结束 redraw；false 实时 redraw
}
```

**没有** `workspace / backgroundColor / transparent / syncWithMap / animationMode / enableHover / showGrips / fonts / onLoad / onHover / onMapMove`——这些字段写在 options 里会被静默忽略。视图跟随是硬编码行为（`move`/`zoom`/`rotate` 监听），不需要开关；`minZoom`/`maxZoom` 控制 overlay 在哪个缩放区间显示。

## 4. 关键方法

```ts
class CadMapOverlay {
    constructor(options: CadMapOverlayOptions);
    addTo(map: any, insertId?: string | HTMLElement): Promise<this>;  // insertId 可换容器
    remove(): void;                             // 卸载（移除 DOM + 解绑事件）
    setVisible(visible: boolean, isDisplay?: boolean): void;
    //   isDisplay=true → style.display 显隐；否则走 _add/_remove 真正挂载/卸载

    // 视图
    zoomToExtents(): void;                      // map.fitBounds(cadBounds, {padding:50})
    updateBounds(bounds: CadBoundsLike): void;  // 改 bounds 并重算
    getCadBounds(): number[];                   // [x1,y1,x2,y2]
    redraw(): void;
    getCadView(): MainView | null;              // 拿到底层 MainView 后可调 setTheme 等

    // 实体 / 选择（都直接转发到 Engine）
    addEntities(entities: EntityBase | EntityBase[]): void;  // = Engine.addEntities
    getSelection(): EntityBase[];               // = Engine.editor.PreSS
    setSelection(entities: EntityBase[]): void; // 注意：吃实体对象数组，不是 handle
    clearSelection(): void;

    // 坐标转换（返回对象，不是数组）
    cadToLngLat(x: number, y: number): { lng: number; lat: number };
    lngLatToCad(lng: number, lat: number): { x: number; y: number };
    lngLatToCad(lngLat: { lng: number; lat: number }): { x: number; y: number };

    // 公共字段：minZoom / maxZoom / enableSelection / smoothAnimation 可直接读写
}
```

**不存在**：`isVisible / loadMap / closeMap / zoomToBounds / flyTo / removeEntities / setAlpha / setThemeMode`。删除实体走 `Engine.eraseEntities`；改主题经 `getCadView().setTheme(...)` / `setThemeColors(...)`。

## 5. 选择联动

```js
cadOverlay = new CadMapOverlay({
    enableSelection: true,
    onSelectionChanged: entities => {          // 实参是 EntityBase[]
        if (entities.length === 0) return popup?.remove();
        const e = entities[0];
        // EntityBase 没有通用 center 属性；取包围盒中心
        const box = Engine.getBoundsByEntities([e]);
        const { lng, lat } = cadOverlay.cadToLngLat(box.center.x, box.center.y);
        popup = new vjmap.Popup({ closeOnClick: false })
            .setLngLat([lng, lat])
            .setHTML(`<b>${e.entityType}</b><br/>图层: ${e.layer}`)   // entityType 是 getter，不要加 ()
            .addTo(map);
    }
});
```

## 6. 坐标转换实战

```js
import { Point2D, BoundingBox } from 'vjcad';

// 地图点击 → 找到 CAD 实体
map.on('click', e => {
    const { x, y } = cadOverlay.lngLatToCad(e.lngLat.lng, e.lngLat.lat);
    // BoundingBox 构造收两个 Point2D 角点，不是 4 个数字
    const hits = Engine.spatialIndexManager.search(new BoundingBox(
        new Point2D(x - 1, y - 1), new Point2D(x + 1, y + 1)
    ));
    if (hits[0]) cadOverlay.setSelection([hits[0]]);   // 传实体对象，不是 id 字符串
});

// CAD 实体 → 在地图上画 marker
ents.forEach(e => {
    const box = Engine.getBoundsByEntities([e]);
    const { lng, lat } = cadOverlay.cadToLngLat(box.center.x, box.center.y);
    new vjmap.Marker().setLngLat([lng, lat]).addTo(map);
});
```

## 7. 实体添加 / 删除

```js
const line = new LineEnt([0, 0], [100, 100]);
line.setDefaults();
line.color = 1;
cadOverlay.addEntities([line]);     // 等价 Engine.addEntities，自动 redraw

// 删除没有 overlay.removeEntities——直接走引擎
Engine.eraseEntities([line]);
cadOverlay.redraw();
```

`addEntities` 走的是 vjcad 引擎（内部就是 `Engine.addEntities` + redraw）。

## 8. 视图同步（`smoothAnimation`）

跟随是硬编码的（监听地图 `move`/`zoom`/`rotate` + `zoomend`），没有 `syncWithMap`/`animationMode` 开关；只有渲染策略可选：

| `smoothAnimation` | 说明 |
|---|---|
| `true`（默认） | 拖动/缩放中用 CSS transform 平滑跟手，`moveend` 后 redraw |
| `false` | 每次 move/zoom 事件实时 redraw——响应即时但可能抖动 |

## 9. 多图叠加（不建议）

vjcad 按**单视图**设计：`Engine` 的文档集合、当前文档、选择集、命令、事件都是全局静态的，每个 overlay 的 `addTo` 都会新建一个 MainView 并让 `Engine.view` 指向**最后初始化的那个**。所以多个 `CadMapOverlay` 并不独立——它们共享文档 / 选择 / 撤销，`addEntities`、`openDbDoc` 等操作落在同一个 Engine 上。需要多张图同屏，用 vjmap 自己的图层（WMS / 瓦片）叠其余图纸，只保留一个 `CadMapOverlay` 做可编辑层。

```js
// overlay 视图固定 backgroundAlpha:0（构造时写死），无需自己传 transparent
// 想调透明度，对 getCadView() 的 DOM 操作，没有 setAlpha
cadOverlay.getCadView()?.style.setProperty('opacity', '0.6');
```

## 10. 主题切换

```js
// 构造时用 themeMode / themeColors；运行期走底层 MainView
cadOverlay.getCadView()?.setThemeColors({ /* ThemeColorsPartial */ });
cadOverlay.getCadView()?.resetThemeColors();
// 没有 overlay.setThemeMode——themeMode 只在构造时生效
```

地图换主题时同步换 CAD 主题，避免黑底白线 + 白底地图的对比错乱。

## 红旗

- `bounds` 错了（与 mapsize 不一致）→ CAD 内容会被拉伸 / 偏移；建议用 vjmap 的工具计算图纸地理范围
- overlay 透明来自内部 `backgroundAlpha:0`（写死），无 `transparent`/`backgroundColor` 选项
- 多个 `CadMapOverlay` 并不独立：`map._webcadOverlays` 只是登记表，Engine 是单视图全局状态（见第 9 节），后 `addTo` 的接管 `Engine.view`；`remove()` 只把 MainView 从 DOM 摘下并解绑地图事件，**不销毁 MainView**——要彻底释放先 `await cadOverlay.getCadView()?.destroy()` 再 `remove()`
- `cadToLngLat` / `lngLatToCad` 只是转调 vjmap 的 `map.toLngLat / fromLngLat`（投影由 `map.attach(svc, prj)` 决定）；地图不是 attach 过的 `vjmap.Map` 时会退化为原样返回坐标
- `setSelection` / `getSelection` 与 `Engine.editor` 的选择集是**同一份**（`PreSS`/`ssSetFirst`）——不要和 vjmap 侧自己的选择逻辑混用
- `addTo(map)` 必须在 vjmap 地图完全 `load` 之后调用；返回 `this` 可链式
- 地图 zoom 超出 `[minZoom, maxZoom)` 区间时 overlay 自动卸载/重挂（`_checkVisible`）

## 参考（官方文档 MCP）

- 文档：`vjcad/doc/ui/map-overlay.md`
- 示例：`vjcad/example/basic/05mapoverlay.js`
