> **本文回答**：`vjmap.SvgOverlay` / `DivOverlay` —— 随地图缩放的 SVG / DIV 覆盖物。

## 一、定位

这是**非 WebGL 的 DOM 覆盖物**。适合：

- 小量交互强的 SVG（动画、鼠标交互、组态图）
- 贴图（png / jpg）按四点精确定位
- 框架化组件（Vue/React）作为地图装饰

**缺点**：

- 性能远低于 WebGL 层
- 数量多会卡（建议单图 <20）
- 倾角大时可能"失真"或"消失"（SVG 有 `maxPitch`）

## 二、SvgOverlay 最小示例

```js
const svgOverlay = new vjmap.SvgOverlay({
  maxZoom: 5,      // 超过 zoom=5 自动隐藏
  maxPitch: 60     // pitch ≥ 60 度时隐藏（显示条件 pitch < maxPitch）
});
svgOverlay.addTo(map);

svgOverlay.addElement({
  // 坐标 / 长度须用 {{}} 占位（CAD 坐标、CAD 长度），裸数字是会随刷新错位的 SVG 像素
  html: `<circle ${vjmap.SvgOverlay.attr_cx_cy_r(pt.x, pt.y, 10)} style="fill:red"/>`,
  event: container => {
    const ele = container.querySelector("circle");
    ele.addEventListener("click", () => alert("点了"));
  }
});
```

## 三、SvgOverlay 选项

| 选项 | 说明 |
|---|---|
| `minZoom` / `maxZoom` | 可见 zoom 区间 |
| `maxPitch` | 可见最大倾角 |
| `updateDivSize` | 是否随 zoom 放大 div（SVG 需要 true） |
| `maxDivSize` | div 放大上限（超过则像素放大） |
| `noUpdateBoundsWhenMoveend` | moveend 不更新 bounds |
| `divClassName` | 外层 div class |
| `svgMaxWidth` / `svgMaxHeight` | SVG 初始画布大小（默认 1000） |
| `svgOffset` | 初始化偏移（默认 100） |

## 四、增删改 SVG 元素

```js
// 单个
const id = "myCircle";
svgOverlay.addElement({
  html: `<circle id="${id}" ${vjmap.SvgOverlay.attr_cx_cy_r(pt.x, pt.y, 10)} fill="red"/>`,
  event: parent => {
    const el = parent.querySelector(`#${id}`);
    el.addEventListener("click", () => console.log("clicked"));
  }
});

// 批量
svgOverlay.addElements([
  { html: `<circle ${vjmap.SvgOverlay.attr_cx_cy_r(p1.x, p1.y, 50)}/>`, event: () => {} },
  { html: `<line ${vjmap.SvgOverlay.attr_x1_y1_x2_y2(p1.x, p1.y, p2.x, p2.y)} stroke="red"/>`, event: () => {} }
]);

// 删：按元素的 id 字段或 html 原文匹配（不是 html 里写的 DOM id）
svgOverlay.removeElements([id1, id2]);

// 更新：按 id/html 定位后 Object.assign 合并（不是先删再加）
svgOverlay.updateElements([{ id, html: "<circle ...>", event: () => {} }]);

// 直接操作 SVG container：仅临时生效——每次 moveend 都按 elements[].html 重建 innerHTML，改动会被覆盖
const svg = svgOverlay.getSvgContainer();
svg.querySelector("#myCircle").setAttribute("fill", "blue");
// 持久修改用 updateElements([{ id, html }])

// 获取所有元素
const all = svgOverlay.getElements();
```

## 五、SVG 辅助静态函数

`attr_*` 返回的不是字面属性，而是 **`{{}}` 模板占位符**——`update()` 时把 `{{x,y}}` 地理坐标换算成像素、`{{n}}` 长度按比例缩放：

```js
vjmap.SvgOverlay.attr_cx_cy_r(10, 20, 5);   // '{{cx=10,cy=20}} r="{{5}}"'
vjmap.SvgOverlay.attr_x1_y1_x2_y2(0, 0, 9, 9); // '{{x1=0,y1=0}} {{x2=9,y2=9}}'
vjmap.SvgOverlay.attr_x_y_w_h(1, 2, 3, 4);  // '{{x=1,y=2}} width="{{3}}" height="{{4}}"'
vjmap.SvgOverlay.attr_x_y(5, 6);            // '{{x=5,y=6}}'
vjmap.SvgOverlay.attr_fontsize(14);         // 'font-size="{{14}}"'
vjmap.SvgOverlay.attr_length(8);            // '{{8}}'
vjmap.SvgOverlay.attr_point(geoPt);         // '{{d=12.5,-3.25}}'（默认带 `d=` 键名）
vjmap.SvgOverlay.attr_point(geoPt, true);   // '{{12.5,-3.25}}'（joinComma 传 true 才是裸 '{{x,y}}'——语义反直觉，见 min.js）
vjmap.SvgOverlay.attr_points([p1, p2]);     // 'points="{{10,20}} {{30,40}}"'
vjmap.SvgOverlay.attr_path([p1, p2]);       // 'M{{12.5,-3.25}} L{{30,40}}'（内部走 joinComma=true 分支，顶点为裸形态）
```

用法：`html: '<circle ' + attr_cx_cy_r(cx, cy, r) + ' fill="red"/>'`。

⚠️ **占位符数字必须 ≥2 位或带小数点**：替换正则是 `\d+[.]?\d+`，`{{5}}`、`{{5,6}}`（单个一位数）**不匹配会原样进 DOM** 报 `Expected length, "{{5}}"`。写 `{{5.0}}` 或 `{{55}}` 才替换。

## 六、DivOverlay 最小示例（贴图片）

```js
const image = document.createElement("img");
image.style.position = "absolute";
image.style.left = "0";
image.style.top  = "0";
image.style.width  = "1280px";
image.style.height = "905px";
image.style.opacity = "0.8";
image.src = "/images/park.jpg";
image.style.pointerEvents = "none";

const divOverlay = new vjmap.DivOverlay({
  bounds: fourPoints,    // 四个 CAD 点（左下 / 左上 / 右上 / 右下）
  element: image,
  width: 1280,
  height: 905,
  minZoom: 5,
  maxZoom: 15
});
divOverlay.addTo(map);
```

## 七、DivOverlay 选项

| 选项 | 说明 |
|---|---|
| `bounds` | 四个点坐标数组 `[[x,y],[x,y],[x,y],[x,y]]` 或 GeoBounds |
| `element` | HTML 元素 |
| `width` / `height` | 元素原生大小（像素） |
| `minZoom` / `maxZoom` | 可见范围 |
| `maxPitch` | 可见最大倾角 |
| `updateDivSize` | 随 zoom 放大 div |
| `maxDivSize` | div 放大上限 |

## 八、DivOverlay 动态更新

```js
divOverlay.updateBounds([pt1, pt2, pt3, pt4]);   // 拖动四角调整
divOverlay.updateSize(w, h);                     // 改原生大小

divOverlay.setVisible(false);                    // 隐藏：从文档摘除 DOM
divOverlay.setVisible(true);                     // 显示：插回 DOM
divOverlay.setVisible(false, true);              // 仅切 display:none（DOM 保留）
divOverlay.setVisible(true, true);               // 仅切 display:""
// 两种方式须成对使用：setVisible(false) 摘除后再 setVisible(true, true) 只改 display，元素仍不在文档里
divOverlay.remove();                             // 彻底移除
```

## 九、组态图场景（DIV 内装 SVG）

```js
const svgStr = `
  <svg xmlns="http://www.w3.org/2000/svg"
       viewBox="0 0 800 600"
       preserveAspectRatio="xMinYMin meet">
    <!-- 组态内容 -->
  </svg>`;

const el = document.createElement("div");
el.style.position = "absolute";
el.style.width = "800px";
el.style.height = "600px";
el.innerHTML = svgStr;

new vjmap.DivOverlay({
  bounds: mapBounds,   // GeoBounds 直接传（也接受四点数组）
  element: el,
  width: 800,
  height: 600,
  updateDivSize: true,       // SVG 随缩放放大
  maxDivSize: 6000
}).addTo(map);
```

## 十、Vue/React 组件作为 DivOverlay

```js
// Vue3
const host = document.createElement("div");
host.style.position = "absolute";
const app = Vue.createApp(MyComp);
app.mount(host);

new vjmap.DivOverlay({
  bounds, element: host, width: w, height: h
}).addTo(map);

// 卸载时
app.unmount();
divOverlay.remove();
```

## 十一、对比

| 需求 | 首选 |
|---|---|
| 几千个标志 | WebGL Symbol |
| 一张背景贴图按四角对齐 | DivOverlay |
| 组态图 / 设备示意 | DivOverlay (内嵌 SVG) |
| 简单可交互几何（< 50 个） | SvgOverlay |
| 大量可交互几何 | 用 WebGL Polygon / Polyline + hover |
| Vue/React 卡片 | Marker |
| 气泡弹窗 | Popup |

## 十二、常见坑

1. **倾斜时看不见** → `maxPitch` 默认 90（显示条件 `pitch < maxPitch`），引擎最大倾角 85，默认不会因倾角隐藏；**设 `maxPitch: 85` 反而会在 85° 时隐藏**。大倾角下变形 / 消失是 CSS 3D 矩阵变换的局限，用 `map.setMaxPitch(...)` 限制倾角或改用 WebGL 图层。
2. **多个 DivOverlay 错位** → 每个子 element 记得 `position: absolute; left:0; top:0;`。
3. **SVG 超大 zoom 失真** → 设 `maxDivSize` 控制；或切成切片贴图。
4. **DivOverlay 挡鼠标** → `pointerEvents: "none"`，或只对需要响应的子元素保留 pointer。
5. **Vue unmount 前 divOverlay 没删** → 容易内存泄漏；`onBeforeUnmount` 做清理。
6. **`addElement` 必须在 `addTo(map)` 之后**：先于挂载调用会因内部 `update()` 访问 `this._map.getZoom` 抛 `TypeError`（`_map` 还是 undefined）。
7. **DivOverlay 的 `bounds` 选项类型严格**：只接受 `GeoBounds` 或 `[4×GeoPointLike]` 四角点数组 + `width`/`height` 必填；传 `"[x,y,x,y]"` 字符串抛 `Invalid input coordinate: [`（文本格式要走 `vjmap.GeoBounds.fromString`）。
8. **抛错的 overlay 必须 `remove()`**：构造/addTo 抛异常后若残留，该对象仍挂在地图 zoom/move 事件链上持续抛错，**阻断后续监听器**（残留坏 DivOverlay 导致 `zoomend` 不再派发）。
9. **`map.removeMarkers()` 事件载荷不是数组**：`removeMarkers` 事件回调拿到的是 Evented `fire` 出来的**事件对象**（`e.type === "removeMarkers"`），markers 数组被 `extend` 展开成数字键 `e[0]`、`e[1]`……——取 `e.markers`/`e.data` 都是 `undefined`，遍历数字键才是移除的 Marker 列表。`noIncludeDivOverlay` / `noIncludeSvgOverlay` 为双否定参数：传 `true` 才**排除**对应覆盖物不被清理。
