# map-ol-utils 中文文档

规范地址：https://npmcn.com/package/map-ol-utils/

版本：1\.2\.10
npm 元数据更新时间：2026-11-04T02:46:02.690Z
中文译文更新时间：2026-11-04T03:12:24.267Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/map-ol-utils
原文对照：https://npmcn.com/package/map-ol-utils/?lang=original

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：204 kB，7 个文件
- 模块格式：ESM + CJS
- TypeScript 类型：包内自带
- Node.js 要求：^22\.18\.0 \|\| ^24\.0\.0 \|\| \>=26\.0\.0
- 运行依赖：4 个
- peer 依赖：ol ^10\.10\.0
- 首次发布：2025-04-10T13:54:37.137Z
- 最新版本发布：2026-11-04T02:46:02.518Z
- 已发布版本数：129（近 12 个月 30 个）

## 中文简介

OpenLayers实用程序与浏览器端天地图私有VTS的加载和转换

## 安装

```sh
npm install map-ol-utils
```

也可以使用：`pnpm add map-ol-utils`、`yarn add map-ol-utils`、`bun add map-ol-utils`

## README

# map-ol-utils

> 目录：`packages/map-utils` ｜ npm 包名：`map-ol-utils`

基于 [OpenLayers](https://openlayers.org/) 的地图工具库，封装了地图实例管理、底图创建、矢量/面/线/聚合/海量点图层、要素交互选择、视野自适应、WMTS 接入，以及天地图私有 VTS 的浏览器加载与标准 MVT 转换能力。

## 特性

-   **单例与独立地图**：默认沿用模块单例；地图相关工具可显式传入目标地图，隔离选择、图层、覆盖物与视野。
-   **多种底图**：杭州规自瓦片、全国天地图（XYZ，支持在线与本地栅格）、浙江省天地图（XYZ）。
-   **业务矢量图层**：根据业务数据自动推断几何类型（线/面）并生成 GeoJSON 图层，支持着色映射、点击高亮。
-   **海量点 MassMarks**：基于状态值的多套样式（默认 + 激活），可附带伴生线/面图层。
-   **聚合图层**：基于 `ol/source/Cluster`，半径/颜色随聚合数量动态计算。
-   **坐标系转换**：内部统一通过 [`gcoord`](https://github.com/hujiulong/gcoord) 转换到 `WGS84`（`EPSG:4326`）。
-   **事件总线**：内置 `mitt` 实例 `emitter`，用于派发要素选中等事件。
-   **扩展能力**：提供天地图组合、栅格着色、标记、绘制量算、GeoJSON 图层和多图层选择能力。
-   **私有 VTS 转换**：通过 `tdt-vts-core` 复用 PK 编解码、私有 Protobuf/MVT 转换和 Web Worker 解码，本包只保留 OpenLayers 适配。

## 项目结构

```text
packages/map-utils/
├── src/
│   ├── core/                    # 类型、常量、事件总线与地图上下文
│   ├── interaction/             # 绘制和要素选择交互
│   ├── layer/                   # 底图、矢量、聚合、海量点、WMTS 与图层生命周期
│   ├── map/                     # 地图实例管理与视野适配
│   ├── overlay/                 # Overlay 与动画标注
│   ├── tdt-vts/                 # 天地图私有 VTS 的公共重导出与 OL 适配
│   └── index.ts                 # 唯一公共 API 入口
├── test/
│   ├── architecture.test.ts     # 模块边界与架构约束
│   └── compatibility.test.ts    # 公共 API 兼容性测试
├── package.json                 # 包入口、依赖和构建脚本
├── tsconfig.json                # TypeScript 配置
├── tsdown.config.ts             # ESM/CJS 与类型声明构建配置
└── README.md                    # 安装、API 与使用约定
```

航线内部按职责拆分：`interaction/route.ts` 维护航线状态、公开交互入口、事务与模式生命周期；`interaction/route-control.ts` 管理实时飞行器、动作集合和动画帧；`interaction/route-editor.ts` 管理选择、拖动、插点、删除、高度调整与编辑资源清理；`interaction/route-waypoints.ts` 校验导入数据、创建航点、准备属性合并并转换导出数据；`route-input.ts` 管理键盘和面板输入、输入会话与地图指针恢复；`route-style.ts` 提供默认样式。调用方从包根导入 `createRouteController`，内部模块不作为公共入口。

## 安装

```bash
pnpm add map-ol-utils ol gcoord
```

Node.js 支持 `^22.18.0 || ^24.0.0 || >=26.0.0`。ESM 与 CommonJS 入口共享同一运行时；CommonJS 使用 Node 的 `require(esm)` 加载能力，避免重复地图单例和事件总线。浏览器宿主继续使用 ESM。

`ol` 是**对等依赖**（要求 `^10.10.0`），需由宿主项目安装。`tdt-vts-core` 是普通依赖，由包管理器自动安装。

## 快速开始

```ts
import {
  createMapInstance,
  createBusinessLayer,
  setFitViewByLayers,
  emitter
} from 'map-ol-utils'

const map = createMapInstance({ center: [120.15, 30.28], zoom: 11 })
map.setTarget('map') // 容器通过 setTarget 绑定，config 仅透传给 ol/View

const layer = createBusinessLayer(dataList, {
  key: 'coordinates',
  isLine: false,
  openClick: true,
  layerName: 'biz-layer',
  fillColor: '#4247DF',
  strokeColor: '#1DEBFF'
})
map.addLayer(layer)
setFitViewByLayers([layer], map) // 多个业务图层加载完成后可一起传入

emitter.on('selected-feature', (data) => console.log('选中要素：', data))
```

## 天地图私有 VTS

传入 Mapbox/MapLibre Style v8 JSON 地址或已加载的样式对象，创建 OpenLayers 图层：

```ts
import { createTdtVtsLayer } from 'map-ol-utils'

const layer = await createTdtVtsLayer({
  token: 'YOUR_TDT_TOKEN',
  hosts: ['https://tile0.tianditu.gov.cn'],
  style: '/styles/tdt-style.json'
})

map.addLayer(layer)
layer.dispose() // 同时释放解码 Worker
```

`hosts` 与 `style` 均为必填项。传入地址时，本包会请求并校验 Style v8 JSON，服务端需允许当前页面跨域访问；也可以直接传入包含 `sources` 和 `layers` 的样式对象。

业务调用只需要 `createTdtVtsLayer()`；底层转换 API 保留用于兼容已有调用。

* * *

# 接口（API）详解

> 约定：独立工具函数默认操作 `createMapInstance()` 创建的模块单例；`createMap()` 返回独立地图，其挂载的图层方法只操作自身。下述接口的可选 `mapInstance` 默认为单例；用于事件回调时显式调用，例如 `@click="resetActiveFeature()"`，避免事件对象被当成地图。

```ts
const left = createMap({ center: [120, 30] })
const right = createMap({ center: [121, 31] })
left.setTarget('left-map')
right.setTarget('right-map')
const layer = createBusinessLayer(
  dataList,
  { key: 'coordinates', isLine: false, openClick: true },
  left
)
left.addLayer(layer)
resetZoomAndCenter(undefined, left) // 恢复 left 自己的初始中心
disposeMap(left) // 不影响 right 或模块单例
```

## 一、地图实例（`map/create-map.ts`）

### `createMapInstance(config?)`

创建并缓存地图单例；若已存在则直接返回，不会重建。

```ts
function createMapInstance(config?: Record<string, any>): CustomMap
```

| 参数 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `config` | `object` | `{}` | 透传给 `createMap` → `ol/View` 的视图配置 |

返回：`CustomMap`（OpenLayers `Map` 的扩展，附带 `getLayerByName` / `removeLayerByName` / `changeLayersByNames` 方法与 `activeBaseLayer` 字段）。

```ts
const map = createMapInstance({ center: [120.15, 30.28], zoom: 11 })
map.setTarget('map')
```

### `createMap(config?)`

始终新建一个 `CustomMap`。默认挂载杭州规自暗色底图，禁用旋转/双击缩放，并在 `pointermove` 时根据是否命中要素切换鼠标指针；挂载的图层查询、切换和删除方法绑定到当前地图。`config` 与以下默认值合并后作为 `ol/View` 选项：

鼠标指针更新按帧合并；延迟回调执行时若地图已解除目标元素，会跳过命中检测与样式更新。

```ts
function createMap(config?: Record<string, any>): CustomMap
// 默认 View 配置：
// { zoom: 9.4, maxZoom: 18, minZoom: 9.4, center, baseLayer: 'vec-layer',
//   projection: 'EPSG:4326', layers: [createBaseLayer()],
//   constrainResolution: false, smoothExtentConstraint: false, tk }
```

| 参数 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `config` | `object` | `{}` | `ol/View` 配置；可额外传 `layers` 覆盖默认底图、`baseLayer` 标记当前底图名 |

```ts
const map = createMap({ zoom: 12, center: [121.43, 28.66] })
```

### `getMapInstance()`

```ts
function getMapInstance(): CustomMap
```

返回当前单例（等价于导出的 `instance` 变量）。

### `disposeMap(mapInstance?)`

```ts
function disposeMap(mapInstance?: CustomMap): void
```

销毁指定地图，省略参数时销毁单例：清空覆盖物与交互，从实际所属的图层组移除叶子图层，释放渲染器、数据源和库创建的监听；保留旧渲染帧的 LayerState。`setTarget('')` 后销毁地图，仅目标是当前单例时清空 `instance`。未初始化或重复调用安全。

### `resetMap(config?, mapInstance?)`

```ts
function resetMap(
  config?: { center?: Coordinate; isClearAll?: boolean },
  mapInstance?: CustomMap
): void
// 未指定 center 时使用目标地图创建时的中心；isClearAll 保留兼容，仍清理全部业务图层。
```

清除所有非底图图层（内部调用 `removeAllLayers`）并复位缩放与中心。

### `resetZoomAndCenter(center?, mapInstance?)`

```ts
function resetZoomAndCenter(center?: Coordinate, mapInstance?: CustomMap): void // 默认目标地图初始中心
```

动画（1s）复位到默认缩放 `9.4` 与指定中心。

### `instance`

```ts
let instance: CustomMap // 导出变量；未创建时为 undefined
```

* * *

## 二、底图与图层名管理（`layer/base-layer.ts`）

### `createBaseLayer(layerName?)`

杭州规自瓦片底图（`EPSG:4326`，XYZ，5 路负载）。

```ts
function createBaseLayer(layerName?: HzsyType): Tile
// HzsyType = 'hzsyraster' | 'hzsyvector' | 'hzsyvector_dark'（默认 'hzsyvector_dark'）
```

图层附带属性 `{ _isBaseLayer: true, _layerName: layerName }`，`zIndex: -5`。

```ts
const base = createBaseLayer('hzsyvector')
map.addLayer(base)
```

### `createMapWorldLayer(layerName, tk?, config?)`

全国天地图经纬度 XYZ 图层，支持在线服务和下载器生成的本地栅格。在线仍访问官方 WMTS GetTile 接口，将 `{z}`、`{x}`、`{y}` 填入请求参数；前端不使用 `WMTS` / `WMTSTileGrid`，也不访问 DataServer。

```ts
function createMapWorldLayer(
  layerName: WorldLayerType,
  tk?: string,
  config?: WorldLayerLocalOptions
): Tile<XYZ>
// WorldLayerType = 'vec_c' | 'cva_c' | 'img_c' | 'cia_c' | 'ter_c' | 'cta_c' | 'ibo_c'

interface WorldLayerLocalOptions {
  url: string // 必须包含 {z}、{x}、{y}，扩展名与实际文件一致
}
```

| 参数 | 类型 | 默认 | 说明 |
| --- | --- | --- | --- |
| `layerName` | `WorldLayerType` | — | 形如 `img_c`，对应 `EPSG:4326` 经纬度矩阵 |
| `tk` | `string` | 内置默认令牌 | 原有第二参数，在线访问的 Key；本地模式可传 `undefined` |
| `config` | `WorldLayerLocalOptions` | 无 | 新增第三参数，提供本地或自托管的 XYZ 地址，不附加 Key |

```ts
// 现有在线调用方式保留
map.addLayer(createMapWorldLayer('img_c', '你的天地图令牌'))

// 本地影像与注记，无需 TK；先将下载目录通过 HTTP 提供在 /tiles 下
map.addLayer(
  createMapWorldLayer('img_c', undefined, {
    url: '/tiles/img_c/{z}/{x}/{y}.jpg'
  })
)
map.addLayer(
  createMapWorldLayer('cia_c', undefined, {
    url: '/tiles/cia_c/{z}/{x}/{y}.png'
  })
)
```

瓦片网格按 `[-180, 90]` 为原点，256 像素瓦片，第 `z` 级分辨率为 `360 / (256 * 2 ** z)`；支持第 1～19 级，目录层级直接对应 `{z}`，不再使用 WMTS 的矩阵索引偏移。这里只接收 `*_c` 经纬度栅格，不能传入 `*_w` 或 VTS 数据。行号从北向南增加，不使用 `{-y}`。

本地模式关闭横向世界重复，不自动回退在线服务；仅已下载的区域与层级有图，未下载部分会返回 404。地址可以是同源路径或静态服务器的 HTTP(S) URL；跨域服务器需允许 CORS。在线模式保持世界重复。图层保留 `_isBaseLayer: true`、`_layerName`、`zIndex: -10`，继续支持按名称查询与非底图清理。

底层 `getSource()` 现在返回 `XYZ`；直接使用 `WMTS` 专有方法（如 `getMatrixSet()`、`getRequestEncoding()`）或旧网格索引的调用方需要迁移。普通 `createMapWorldLayer(layerName, tk)`、挂载及图层管理调用无需调整。回归见 `test/world-layer.test.ts` 与示例包 `map-utils-world-layer.spec.ts`。

### `createZheJiangLayer(layerName?, tk?)`

浙江省天地图 XYZ 图层。

```ts
function createZheJiangLayer(layerName?: ZheJiangType, tk?: string): Tile
// ZheJiangType = 'emap' | 'emap_lab' | 'imgmap' | 'imgmap_lab'（默认 'emap'）
```

### `getLayerByName(name, mapInstance?)`

```ts
function getLayerByName(
  name: string,
  mapInstance?: CustomMap
): Layer<Source> | undefined
```

按图层 `_layerName` 属性或 `className` 查找首个匹配图层。

### `removeLayerByName(name, mapInstance?)`

```ts
function removeLayerByName(name: string, mapInstance?: CustomMap): void
```

查找所有匹配图层，逐一释放其数据源、从地图移除并执行 `dispose`。

### `changeLayersByNames(activeNames, allLayerNames)`

```ts
function changeLayersByNames(
  activeNames: string[],
  allLayerNames: string[]
): void
```

在 `allLayerNames` 范围内，按 `activeNames` 是否包含来逐一 `setVisible`，实现批量显隐切换。

```ts
changeLayersByNames(['poi-layer'], ['poi-layer', 'line-layer', 'face-layer'])
```

* * *

## 三、矢量图层（`layer/vector-layer.ts`）

### `createBusinessLayer(dataList, config, mapInstance?)`

坐标转换复制坐标树，不修改传入数组，也支持 Vue 响应式数组。二维闭合单环自动包装为 Polygon 的环数组；带洞面与 MultiPolygon 保留原层级。

核心矢量图层构造函数：将业务数据组装为 GeoJSON（自动推断线/面几何类型），生成默认/激活样式并写入要素属性，可选开启点击交互。

`VectorConfig` 复用 `Omit<VectorLayerOptions, 'source' | 'map'>`，支持 `visible`、`opacity`、`zIndex`、`extent`、缩放/分辨率范围、`declutter`、`renderBuffer`、`properties` 等原生配置。默认 `zIndex: 5`，显式 `className` 优先于 `layerName` 派生的类名；业务图层名仍保存在 `_layerName`。内部数据源不可覆盖，图层需由调用方 `map.addLayer()` 挂载；`openClick` 开启时选择交互已加入所属地图。

原生 `style` 按 OpenLayers 规则作为图层级样式：业务 Feature 自身的默认/激活样式仍优先，追加数据也遵循此规则。使用 `strokeColor`、`fillColor` 和 `active*` 配置业务样式；`properties` 中的同名内部标记会由库覆盖。

```ts
function createBusinessLayer(
  dataList: MassData[],
  config: VectorConfig,
  mapInstance?: CustomMap
): VectorLayer
```

| `config` 关键字段 | 类型 | 说明 |
| --- | --- | --- |
| `key` | `string` | 数据中存放坐标数组的字段名 |
| `isLine` | `boolean` | 是否线图层（影响是否生成填充样式） |
| `layerName` | `string?` | 业务图层名，也是未配置 `className` 时的默认类名 |
| `visible` / `opacity` / `zIndex` | `boolean?` / `number?` / `number?` | 原生可见性、整体透明度和层级，默认 `true` / `1` / `5` |
| `openClick` | `boolean?` | 是否创建单击选择交互 |
| `colorKey` / `colorMap` | `string?` / `Map<string,string>?` | 按数据字段值映射颜色 |
| `fillColor` / `strokeColor` | `string?` | 填充 / 描边色 |
| `fillOpacity` / `strokeWeight` | `number?` | 填充透明度 / 线宽 |
| `activeFillColor` / `activeStrokeColor` / `activeStrokeWeight` | … | 激活态样式 |
| `crsForm` | `CRSTypes?` | 源坐标系（默认 `WGS84`） |
| `isAutoFit` | `boolean?` | 选中后是否自动居中 |

图层附带属性：`_isCustom`、`_isVectorLayer`、`_isAutoFit`、`_vectorConfig`、`_layerName`、`_dataList`、`_uuidSet`。

```ts
const layer = createBusinessLayer(dataList, {
  key: 'coordinates',
  isLine: false,
  openClick: true,
  layerName: 'area',
  visible: true,
  opacity: 0.8,
  zIndex: 8,
  colorKey: 'level',
  colorMap: new Map([
    ['1', '#f00'],
    ['2', '#0f0']
  ]),
  fillOpacity: 0.3,
  strokeWeight: 2
})
map.addLayer(layer)
```

### `createVectorLineLayer(dataList?, config, mapInstance?)` / `createVectorFaceLayer(dataList?, config, mapInstance?)`

`createBusinessLayer` 的便捷包装，分别固定 `isLine: true` / `isLine: false`。

```ts
function createVectorLineLayer(
  dataList?: MassData[],
  config: VectorConfig,
  mapInstance?: CustomMap
): VectorLayer
function createVectorFaceLayer(
  dataList?: MassData[],
  config: VectorConfig,
  mapInstance?: CustomMap
): VectorLayer
```

### `appendFeaturesByLayer(targetLayer, dataList, crsForm?)`

向已有矢量图层追加要素（增量更新其 `_uuidSet` 与 `_dataList`），复用图层 `_vectorConfig` 生成样式；未传 `crsForm` 时继承图层配置的源坐标系。

```ts
function appendFeaturesByLayer(
  targetLayer: VectorLayer,
  dataList: MassData[],
  crsForm?: CRSTypes
): void
```

* * *

## 四、海量点 MassMarks（`layer/mass-marks.ts`）

### `buildMassMarksStyles(getImgUrlFn)`

高阶函数：先注入「图标地址生成器工厂」，再按 `IconConfig` 生成「默认 + 激活」两段样式数组（长度 `statusNum * 2`）。

```ts
function buildMassMarksStyles(
  getImgUrlFn: GetImgUrlFn
): (iconConfig: IconConfig) => Style[]

type GetImgUrl = (name: string, ext?: string) => string
type GetImgUrlFn = (baseUrl: string) => GetImgUrl
```

图标地址按 `${markerIcon}-${i}` / `${markerIcon}-${i}-active`（后缀 `.webp`）拼接。

```ts
const buildStyles = buildMassMarksStyles(
  (baseUrl) => (name, ext) => `${baseUrl}/${name}${ext}`
)
const styles = buildStyles({
  baseUrl: '/icons',
  markerIcon: 'alarm',
  markerSize: [32, 32],
  statusNum: 5
})
```

### `createMassMarksLayer(dataList, styleList?, config?)`

```ts
function createMassMarksLayer(
  dataList: MassData[],
  styleList?: StyleLike[],
  config?: Partial<LayerConfig>
): VectorLayer
```

按 `item.style` 索引在 `styleList` 中取样式创建要素图层，默认 `zIndex: 2000`。可选 `config` 透传原生图层选项和 `layerName`，显式 `className` 优先；内部数据源和地图挂载不可由配置覆盖，返回图层需手动挂载。图层附带 `_isMassMarksLayer`、`_layerName`、`_statusNum`、`_dataList`、`_uuidSet` 等属性。

创建与追加均先复制并数值化 `lnglat`；仅显式传入 `crsForm` 时转换为 WGS84，省略时不调用坐标转换。输入坐标及业务属性保持不变。

### `addMassMarksLayer(getImgUrlFn)`

高阶函数，返回的方法会生成样式、创建海量点图层、按 `hasLine` / `hasFace` 附加伴生矢量图层，并自动加入地图。

`IconConfig` 复用 `Omit<VectorLayerOptions, 'source' | 'map'>`，顶层 `visible`、`opacity`、`zIndex`、`className`、缩放范围等原生选项仅作用于海量点图层；伴生线面仍通过 `vectorConfig` 独立配置。默认层级为 `2000`，`layerName` 同时用作未指定 `className` 时的类名。图层级 `style` 遵循 OpenLayers 优先级，点要素由样式表设置的默认/激活样式仍优先，追加/替换保持此行为。`properties` 中的内部标记由库覆盖。

```ts
function addMassMarksLayer(
  getImgUrlFn: GetImgUrlFn
): (
  dataList?: MassData[],
  iconConfig: IconConfig,
  openClick?: boolean,
  mapInstance?: CustomMap
) => VectorLayer
```

| `iconConfig` 字段 | 类型 | 说明 |
| --- | --- | --- |
| `baseUrl` | `string` | 图标根地址 |
| `markerIcon` | `string` | 图标名前缀 |
| `markerSize` | `[number, number]` | 图标尺寸 |
| `statusNum` | `number?` | 状态数（默认 5） |
| `activeScale` | `number?` | 激活放大倍数（默认 1.3） |
| `hasLine` / `hasFace` | `boolean?` | 是否附带线 / 面图层 |
| `isDynamic` | `boolean?` | 是否动态图层（选中派发 `dynamic-marker`） |
| `isAutoFit` | `boolean?` | 选中是否自动居中 |
| `layerName` | `string?` | 图层名 |
| `offset` | `[number, number]?` | 偏移 |
| `marksStyles` | `Style[]?` | 直接指定样式（跳过自动生成） |
| `visible` / `opacity` / `zIndex` | `boolean?` / `number?` / `number?` | 点图层原生配置，默认 `true` / `1` / `2000` |
| `vectorConfig` | `VectorConfig?` | 伴生线/面图层配置覆盖 |

```ts
const add = addMassMarksLayer((baseUrl) => (n, e) => `${baseUrl}/${n}${e}`)
const layer = add(dataList, {
  baseUrl: '/icons',
  markerIcon: 'alarm',
  markerSize: [32, 32],
  statusNum: 5,
  opacity: 0.8,
  zIndex: 2100,
  hasLine: true
})
```

内部 `_dataList` 和 `_uuidSet` 只读使用：追加时原地扩展，不再每批复制全部历史；`clear: true` 使用新集合替换。调用方传入的数组不被追加修改；需要快照时自行复制。直接使用 `createMassMarksLayer(data, styles)` 创建后，追加仍继承同一套默认与激活样式。

### `addFeaturesByLayer(targetLayer, dataList, config)`

向海量点图层增量添加要素（复用图层 `_massMarksStyles`）。

```ts
function addFeaturesByLayer(
  targetLayer: VectorLayer,
  dataList: MassData[],
  config: { clear: boolean }
): void
```

`config.clear` 为 `true` 时先清空数据源再添加。

### `updateMarksLayersByStatus(checkedStatusList?, mapInstance?)`

```ts
function updateMarksLayersByStatus(
  checkedStatusList?: string[],
  mapInstance?: CustomMap
): MassData[]
```

遍历所有海量点图层：`status` 不在勾选列表中的要素设为隐藏样式，否则恢复默认样式。返回当前保留显示的数据数组。

### `changeFeaturesVisibleByIds(layerName, visibleIdSet, mapInstance?)`

```ts
function changeFeaturesVisibleByIds(
  layerName: string,
  visibleIdSet: Set<string>,
  mapInstance?: CustomMap
): void
```

遍历所有同名海量点图层，隐藏业务 `id` 不在集合中的要素，集合中的要素恢复 `unActiveStyle`。此方法只修改样式，不移除要素或清除已有选择。

* * *

## 五、聚合图层（`layer/cluster-layer.ts`）

交互演示：启动 `packages/example`，打开 `/map-utils-cluster`，可在天地图 VTS 底图上配置聚合样式、切换点击模式并查看返回数据。

### `createClusterLayer(dataList, config, mapInstance?)`

聚合与海量点统一使用 `MassData`。业务侧传入 `id`、`status`、`name`、`style`、`lnglat`、`rawData`；需要按 UID 激活时还应传入稳定的 `_uuid`。原 `ClusterData` 已移除，调用方直接改用 `MassData`，不保留兼容别名。

聚合内部复用海量点的 `createFeatures`：复制并数值化数字或字符串二维坐标，仅显式传入 `crsForm` 时转换为 WGS84，省略时不调用坐标转换；不修改输入数据。单点使用业务字段 `style` 作为 `singleStyles` 索引，默认与激活段保持一致。

```ts
function createClusterLayer(
  dataList: MassData[],
  config: ClusterConfig,
  mapInstance?: CustomMap
): VectorLayer

type MassData = {
  _uuid?: string
  id: string
  name: string
  style: number
  status: string
  lnglat: [number, number] | [string, string]
  crsForm?: CRSTypes
  width?: number
  height?: number
  rawData: Record<string, any>
}
type ClusterConfig = {
  distance: number
  minDistance: number
  singleStyles: Style[]
  clusterStyle?: Style
  layerName?: string
  openClick?: boolean
  clickMode?: 'single' | 'all' | 'zoom' | 'spiderfy'
  computedRadius?: (size: number) => number
  buildStyle?: (feature: FeatureLike) => Style
}
```

`buildStyle` 仍优先接管所有样式。未传 `buildStyle` 时，单点按业务 `style` 字段使用 `singleStyles`；多点优先使用 `clusterStyle`，未配置则使用内置的动态半径、色相（`hsla`）和数量文字。

`clusterStyle` 是一个 OpenLayers `Style` 模板，仅用于多点聚合。通过 `Icon` 配置背景图片及大小，通过 `Text` 配置数字颜色、字号、字体和偏移位置；库自动填入聚合数量，无需回调或单点判断。模板中的 `text` 内容会被数量替换，未配置 `Text` 时补充原生默认文字样式。库按数量缓存模板副本，不修改传入的样式；图标大小由模板决定，不应用 `computedRadius`。

```ts
const cluster = createClusterLayer(dataList, {
  distance: 80,
  minDistance: 20,
  singleStyles: [singlePointStyle],
  openClick: true
})
map.addLayer(cluster)
```

自定义聚合气泡图标、数字颜色、字号和位置：

```ts
import { Fill, Icon, Style, Text } from 'ol/style.js'

const cluster = createClusterLayer(dataList, {
  distance: 80,
  minDistance: 20,
  singleStyles: [singlePointStyle, activeSinglePointStyle],
  clusterStyle: new Style({
    image: new Icon({
      src: '/images/cluster.png',
      width: 42,
      height: 48,
      anchor: [0.5, 1] // 底部居中，尖端对齐点坐标
    }),
    text: new Text({
      font: 'bold 16px sans-serif',
      fill: new Fill({ color: '#fff' }),
      textAlign: 'center',
      textBaseline: 'middle',
      offsetX: 0,
      offsetY: -48 / 2 - 4 // 从底部锚点移至图标中心，再向上微调 4px
    })
  }),
  openClick: true
})
map.addLayer(cluster)
```

* * *

### 聚合点击数据

开启 `openClick: true` 后，通过 `clickMode` 决定 `selected-feature` 事件的数据形状：

-   `single`（默认）：保持原行为，仅点击聚合内只有一个点时返回该点的属性对象；点击多点聚合不派发事件。
-   `all`：返回点击位置聚合内全部点的属性数组；只有一个点时也返回长度为 1 的数组。数组元素与原单点事件相同，含 `_uuid`、`name`、`lnglat`、`rawData` 等属性，不是 OpenLayers `Feature` 实例；嵌套业务数据仍应只读使用。
-   `zoom`：点击多点聚合时，用 300ms 动画适配这组点的范围，让聚合随放大自然拆开，不派发多点数据；点击单点仍返回单个属性对象。使用所属地图的最大缩放级别作为上限，同坐标点或达到最大级别后仍可能聚合。
-   `spiderfy`：未达到最大缩放级别时与 `zoom` 一样放大；达到最大级别后仍有多点聚合，**再次点击**才围绕聚合中心圆形展开，并添加连接线。点击展开的点返回原始单点属性对象并收起，原始坐标和业务数据不变，其他聚合组不变。点击空白、移动/缩放/旋转地图、隐藏或移除图层及父图层组、变更数据源、移除交互及销毁地图时清理展开层和监听。

需要“先放大，再展开重合点”时配置 `openClick: true, clickMode: 'spiderfy'`。展开复用 `singleStyles`，不新增图标配置；当前为单圈布局，大量点或靠近视口边缘时可能超出可视范围，未实现螺旋或列表布局。

```ts
const cluster = createClusterLayer(dataList, {
  distance: 80,
  minDistance: 20,
  singleStyles,
  openClick: true,
  clickMode: 'all'
})
map.addLayer(cluster)

emitter.on('selected-feature', (data) => {
  // 事件总线同时接收普通图层的单个对象。
  const points = Array.isArray(data) ? data : [data]
  console.log(points.map((point) => point.rawData))
})
```

仅配置 `clickMode` 不会自动开启点击。该配置不影响普通矢量图层；`single` / `all` 不自动缩放，`zoom` / `spiderfy` 不永久关闭聚合，缩小地图后仍会重新聚合。

## 六、交互与选中（`interaction/select.ts`）

### `createSelectByLayer(vectorLayer, mapInstance?)`

为指定图层创建 `ol/interaction/Select`（单击、`multi: false`、`hitTolerance: 10`），命中要素时切换激活样式并派发 `selected-feature` 事件；聚合图层按创建时的 `clickMode` 返回单点对象、全部点属性数组、放大多点聚合，或在最大级别再次点击时圆形展开。交互会写入图层属性 `_selectInteraction`。

```ts
function createSelectByLayer(
  vectorLayer: VectorLayer,
  mapInstance?: CustomMap
): Select
```

### `setFeaturesActive(selector, active?, mapInstance?)`

按业务 ID 集合或筛选函数批量切换普通业务矢量图层（包括 MassMarks 与伴生线面）的要素样式，以及聚合图层内原始点的高亮。省略地图参数时使用已创建的单例；多地图请显式传入目标地图。

```ts
import { setFeaturesActive } from 'map-ol-utils'

setFeaturesActive(['device-1', 'device-2']) // 顶层业务 id，严格区分字符串与数字
setFeaturesActive(new Set(['device-1', 'device-2']), false) // 恢复默认样式
setFeaturesActive((feature) => feature.get('status') === 'alarm')
setFeaturesActive((feature) => feature.get('_uuid') === 'uuid-1', true, map)
setFeaturesActive(['cluster-point-1', 'cluster-point-2'], true, map) // 聚合内原始点
setFeaturesActive(() => true, false, map) // 恢复目标地图内所有适用要素
```

`selector` 接受只读 `Array<string | number>`、`ReadonlySet<string | number>` 或 `(feature: Feature) => boolean`，`active` 默认 `true`，返回 `void`。集合匹配由 `MassData.id` 写入的顶层 `id`，不读取 `_uuid`、`rawData.id` 或 OpenLayers 的 `getId()`；按其他字段匹配请使用筛选函数。仅更新命中的要素：激活使用 `activeStyle`，取消使用 `unActiveStyle`；缺少目标样式时保持原样，空集合不改变任何要素。同一业务 ID 在多个适用图层中出现时会全部命中。筛选函数应只读取属性。

聚合图层按内层原始 Feature 匹配，不要求 `_uuid` 或 `openClick`。多点聚合保留原气泡与数量，在命中点的原坐标叠加 `singleStyles` 后半组的激活样式；拆成单点时直接显示激活样式，取消后恢复图层原有样式（包括 `buildStyle`）。不修改原始坐标、原始 Feature 样式或业务传入的样式对象，不增加临时图层，也不触发重新聚合。高亮随缩放、移动和原始点几何更新保留；替换成新的 Feature 不继承旧高亮。完全同坐标的高亮点会重叠，`spiderfy` 展开后在各自展开位置显示，展开期间也可批量取消或激活。

渲染缓冲按创建时的聚合距离预留成员与聚合中心的偏移，保证聚合中心在视口外时仍可显示视口内的高亮点；需要动态增大聚合距离时请用新配置重建图层。

此方法只切换外观，不写入选择集合、不触发 `selected-feature`、不移动视野；点击仍遵循图层的 `clickMode`。已有点击交互或 `activeFeatureByUid()` 的选择高亮独立管理，批量取消不清除选择；取消批量高亮请调用本方法并传入 `false`。Marker 图层与没有 `_isVectorLayer` 标记的自定义图层不在处理范围。回归见 `test/cluster-batch-active.test.ts` 与示例包 `map-utils-cluster-batch-active.spec.ts`。

### `activeFeatureByUid(uid, mapInstance?)`

```ts
function activeFeatureByUid(uid: string, mapInstance?: CustomMap): void
```

在已挂载且绑定选择交互（通常配置 `openClick: true`）的矢量图层中，按原始要素的 `_uuid` 激活首个匹配项。普通图层通过 `_uuidSet` 定位；聚合图层直接查找 Cluster 内层数据源，不要求 `_uuidSet`，也不要求该点已经从聚合组中拆开。未命中时不改变当前选择。

聚合点在原始坐标创建一个临时显示副本，使用该点的 `activeStyle`（由 `singleStyles` 后半组提供），只高亮目标点；不移动视野、不改变原始数据、坐标或聚合数量，也不触发整组的放大或展开行为。`selected-feature` 返回目标原始点属性，`clickMode: 'all'` 时为仅含该点的数组。重复激活同一点不重复通知，同一图层切换 UID 时替换旧高亮。

```ts
activeFeatureByUid('point-a', map) // 聚合组中的 point-a 单独显示为激活态
resetActiveFeature(map) // 清理临时高亮与选择
```

聚合高亮要求图层可见且选择交互启用；`resetActiveVector()`、选择集合清空、点击其他位置、视图变化、图层隐藏/移除、数据变化、交互移除及地图销毁都会清理临时显示。高亮仅是当前交互状态，视图或数据更新后需要时可重新调用。原有鼠标点击仍遵循 `clickMode`。

### `resetActiveFeature(mapInstance?)` / `resetActiveVector(mapInstance?)`

```ts
function resetActiveFeature(mapInstance?: CustomMap): void // 清空 overlay + 触发各矢量图层 deselect
function resetActiveVector(mapInstance?: CustomMap): void // 清空 overlay + 清空各图层选择交互的要素集合
```

* * *

## 七、覆盖物与视野（`overlay/overlay.ts`、`map/view-fit.ts`）

### `createOverlay(config?)`

```ts
function createOverlay(config?: import('ol/Overlay.js').Options): Overlay
// 默认：{ id: 'popup-container', positioning: 'top-center' }
```

未显式传 `element` 时，按 `config.id` 取 `document.getElementById(id)` 作为容器。

### `removeOverlays(key?, mapInstance?)` / `removeAllLayers(mapInstance?)`

```ts
function removeOverlays(key?: string, mapInstance?: CustomMap): void // 移除带指定属性键的 overlay（默认按 'position'）
function removeAllLayers(mapInstance?: CustomMap): void // 移除并释放所有非底图（无 _isBaseLayer）图层 + overlay
```

### `createAnimationMarker(imgUrl, data, mapInstance?)`

```ts
function createAnimationMarker(
  imgUrl: string,
  data: MassData,
  mapInstance?: CustomMap
): void
```

以 `data.lnglat`（经 `data.crsForm` → `WGS84` 转换）创建动画标注覆盖物，先移除目标地图已有动画标注；`removeAnimationMarker(mapInstance?)` 可单独清理该地图的动画标注。`data` 中读取 `width` / `height` / `lnglat` / `crsForm`。

### `setFitViewByCoords` / `setFitViewByGeom` / `setFitViewByLayer` / `setFitViewByLayers`

坐标数组为空时直接返回，不访问地图、不改变视野。

```ts
function setFitViewByCoords(
  coords: Coordinate[],
  mapInstance?: CustomMap,
  fitOptions?: FitOptions | AnimationOptions
): void // 单点时 animate 到 zoom 14，多点时 fit 包围盒

function setFitViewByGeom(
  geom: SimpleGeometry,
  mapInstance?: CustomMap,
  fitOptions?: FitOptions
): void

function setFitViewByLayer(
  layer: VectorLayer,
  mapInstance?: CustomMap,
  fitOptions?: FitOptions
): void // 按图层 source 的 extent 自适应

function setFitViewByLayers(
  layers: readonly VectorLayer[],
  mapInstance?: CustomMap,
  fitOptions?: FitOptions
): void // 合并多个矢量图层的 source 范围，一次适配
```

范围适配默认 `padding: [50,50,50,50]`、`duration: 1000`，可由 `fitOptions` 覆盖。

`setFitViewByLayers` 跳过无数据源、空数据源以及空或非有限范围；没有有效范围时直接返回，不访问地图。调用方应在数据加载完成后调用，图层坐标必须与 View 投影一致。按显式传入的图层计算，不筛选可见性、不递归图层组，也不会监听后续数据变化。

适配仍遵守 View 的缩放约束。`createMap` / `createMapInstance` 默认 `minZoom: 9.4`；大范围数据需要在创建地图时调低 `minZoom`（例如 `0`），否则可能无法完整显示。少量点可通过 `fitOptions.maxZoom` 限制放大程度。

```ts
setFitViewByCoords([
  [120.1, 30.2],
  [120.3, 30.4]
])
setFitViewByLayer(layer, undefined, { duration: 500 })
setFitViewByLayers([pointLayer, lineLayer, polygonLayer], map, {
  padding: [50, 50, 50, 300], // 为左侧面板留白
  maxZoom: 16,
  duration: 500
})
```

### `getLayersDataList(mapInstance?)`

```ts
function getLayersDataList(mapInstance?: CustomMap): any[]
```

汇总所有图层 `_dataList` 属性并 `flat(2)`，返回当前地图上的全部业务数据。

* * *

## 八、WMTS（`layer/ogc-utils.ts`）

### `createWmtsLayer(serverUrl)`

柯里化函数：先传 GeoServer 服务地址，返回的方法按 `WmtsSourceOptions` 解析 GetCapabilities 并构建图层。仅成功响应会进入 capabilities 缓存；HTTP 非成功状态直接抛错，后续调用仍可重试。

```ts
function createWmtsLayer(
  serverUrl: string
): (config: WmtsSourceOptions) => Promise<TileLayer>

type WmtsSourceOptions = {
  layerName: string // 形如 'workspace:datastore'
  style?: string
  format?: string // 默认 'image/png'
  version?: string // 默认 '1.1.0'
  matrixSet?: string // 默认 'EPSG:4326'
}
```

```ts
const build = createWmtsLayer('https://geoserver.example.com/geoserver')
const layer = await build({ layerName: 'ws:store', matrixSet: 'EPSG:4326' })
map.addLayer(layer)
```

* * *

## 九、事件总线（`emitter`）

```ts
import { emitter } from 'map-ol-utils' // mitt 实例
```

| 事件名 | 载荷 | 触发时机 |
| --- | --- | --- |
| `selected-feature` | 要素属性对象；聚合图层 `clickMode: 'all'` 时为属性数组 | 要素被选中（`createSelectByLayer` 内触发） |
| `dynamic-marker` | 动画标注配置 | 选中动态图层（`_isDynamic`）要素 1s 后 |

* * *

## 十、扩展能力

以下 API 补充天地图组合、标记、绘制与 GeoJSON 等能力；`createMapInstance`、`createOverlay` 保持原签名；`disposeMap` 及下述地图相关工具增加可选目标地图参数，省略时保持原单例行为。`disposeInstance()` 直接复用 `disposeMap()` 的完整清理语义。

### 天地图组合与栅格处理

```ts
const { baseImgLayer, baseCiaLayer, baseVecLayer, baseCvaLayer } =
  baseLayers('你的天地图令牌')

const recoloredLayer = changeLayer(
  baseImgLayer,
  [255, 255, 255],
  [0.05, 0.55, 0.05]
)
```

-   `baseLayers(tk)`：创建影像、影像注记、矢量、矢量注记四个 XYZ 图层。
-   `changeSource(layer, rgbColorArray, ratioArray, options?)`：创建栅格颜色转换数据源；`options.threads` 可指定线程数。
-   `changeLayer(layer, rgbColorArray, ratioArray, options?)`：把转换数据源包装为 `VectorImageLayer`；支持相同的线程配置。

`options` 类型为 `{ threads?: number }`。未指定时使用浏览器硬件并发数与 4 的较小值（无法读取时为 2），且至少使用 1 个线程。

### 标记与选择

```ts
const markerLayer = createMarkersLayer(markerData, {
  layerName: 'device-layer',
  zIndex: 10,
  opacity: 0.8,
  minZoom: 5,
  declutter: true
})
map.addLayer(markerLayer)

const select = createLayerSelectByNames(['device-layer'], handleSelected)
map.addInteraction(select)
```

-   `createText(item)` / `createIconStyle(item, active?)`：创建标记文本和图标样式。
-   `createMarker(item, config)` / `createMarkersLayer(dataList, config)`：创建单个标记或标记图层。
-   `createElasticMarkerLayer(dataList, config, mapInstance?)`：在 `zoom < 11`、`11 ≤ zoom < 14`、`zoom ≥ 14` 三档间切换 `img`、`smallImg`、`largeImg`，仅跨档刷新样式；选中时保留激活图标，取消选择按所属地图的当前档位恢复。通过库的生命周期方法移除时自动解绑缩放监听。
-   `changeUnselectStyle(featureCollection, mapInstance?)`：清空选择集合并恢复未选中样式。
-   `createLayerSelectByNames(layerNames, selectedHandler?, unSelectHandler?)`：按多个图层名选择标记。
-   `createMoveMarkerLayer(dataItem, config, mapInstance?)`：创建单标记图层并加入指定地图，省略地图参数时使用单例。

### 绘制、GeoJSON 与视野

-   `createDrawInteraction(type, vectorLayer, openTip?)`：创建绘制交互。
-   `createRouteController(config?)`：自动获取地图并挂载航线资源，统一模式、状态订阅、输入与生命周期，默认空闲。
-   `getDrawStyle(feature, drawStyle?)`：为线和面绘制追加长度/面积标签。
-   `createGeoJsonLayer(geoJson, style, config?)`：直接从 GeoJSON 创建矢量图层。
-   `flyToAnimate(center, zoom?, mapInstance?)`：使用两段缩放动画飞到目标点。

相关类型包括 `LayerConfig`、`DataItemConfig`、`ElasticDataItemConfig`。

`LayerConfig` 复用 `ol/layer/Vector` 的 `Options`，排除由封装管理的 `source`、`map`，并要求业务标识 `layerName`。Marker、弹性 Marker、移动 Marker 和 GeoJSON 图层支持透传 `opacity`、`extent`、`minZoom`、`maxZoom`、`declutter`、`renderBuffer` 等原生参数；显式 `className` 优先于 `layerName` 派生的默认类名。普通与弹性 Marker 默认 `visible: true`、`zIndex: 10`，移动 Marker 保留 OpenLayers 默认值。

Marker 的 Feature 自有图标样式优先于图层 `style`。GeoJSON 保留原有合并顺序：`config.style` 覆盖独立 `style` 参数（包括 `null`）；未提供时沿用独立参数及原有默认样式。

自定义 `className` 时，业务 `layerName` 仍保存在 `_layerName`，供 `getLayerByName` / `removeLayerByName` 查询和清理。`createLayerSelectByNames` / `changeLayersByNames` 仍按 CSS `className` 匹配，调用这两个接口时需传入实际类名。

### 航线控制器

先创建地图，再创建航线。`map` 可选，默认使用 `createMapInstance()` 创建的实例；多地图时显式传入所属地图。库自动创建并挂载航线图层和主交互，不需要手动添加交互。初始为 `idle`，不拦截地图操作。航点与操控仅支持 `EPSG:4326` 经纬度。

```ts
import { createMapInstance, createRouteController } from 'map-ol-utils'

const map = createMapInstance({ center: [120.1551, 30.2741], zoom: 17 })
map.setTarget('map')
const route = createRouteController({
  input: { keyboard: true }, // 可选：地图或面板聚焦时响应快捷键
  speed: 10,
  turnSpeed: 60,
  verticalSpeed: 1,
  edit: { mode: 'snap', pixelTolerance: 10 },
  onWaypointAdd(waypoint) {
    console.log(
      '新增航点',
      waypoint.uuid,
      waypoint.indexNo,
      waypoint.longitude,
      waypoint.latitude
    )
  },
  onBeforeWaypointRemove(waypoint) {
    return window.confirm(`删除航点 ${waypoint.indexNo}？`) // false 阻止删除
  },
  onWaypointRemove(waypoint) {
    console.log('已删除普通航点', waypoint.uuid, waypoint.indexNo)
  }
})

// 业务自己的按钮直接调用：
route.setMode('takeoff') // 下一次地图单击确认起飞点，完成后回到 idle
// 确认起飞点后：
// route.setMode('draw')    // 地图点击追加航点
// route.setMode('control') // 从最后记录点开始操控
// route.setFlightConfig({ speed: 15 }) // 可在操控中更新水平速度（米/秒）
// route.setMode('edit')    // 编辑已有航点
// route.selectWaypoint('航点 UUID') // 编辑模式按 UUID 选中普通航点
// route.setMode('idle')    // 停止操作，保留航线

const unsubscribe = route.subscribe((state) => {
  console.log(state.mode, state.waypointCount, state.aircraft)
})
// const data = route.exportWaypoints() // 保存请求由业务负责
// route.clear() // 清空后复用同一实例，配置和输入保留
// unsubscribe() // 可提前取消；destroy 会清理所有订阅
// route.destroy() // 移除本航线的图层、交互和输入监听，不销毁地图
```

### 模式与生命周期

| 模式 | 行为 |
| --- | --- |
| `idle` | 保留数据，停止当前操作 |
| `takeoff` | 下一次地图单击设置或修改起飞点，成功后返回 `idle` |
| `draw` | 单击追加独立航点并依次连线，没有鼠标跟随线 |
| `control` | 实时飞机移动、转向、升降；显式打点才记录航点 |
| `edit` | 选择、拖动普通航点，插入、删除和调整高度 |

`draw`、`control`、`edit` 要求已有起飞点，可通过地图确认或导入非空航点建立。配置和前置校验失败不切换模式；重复进入同一模式不重启飞控、不丢失选择。切换模式由库清理输入、临时飞机、选择及未完成拖动。操控时普通地图点击不追加航点或移动飞机。

`clear()` 清空航点并回到 `idle`，保留实例、样式、编辑配置、当前飞行配置和输入配置。`destroy()` 幂等，永久移除本航线全部资源和订阅；销毁后不能再次切换模式、导入、更新飞行配置或订阅。外部停用、移除交互，或改变地图容器、View 时停止操作并回到 `idle`；重新启用不会自动恢复上一次操作。航线不支持迁移到其他地图。调用方应先销毁航线，再销毁地图。

本次接口统一移除了旧的绘制开关和多个模式切换方法，所有模式通过 `setMode()` 管理，所有编辑配置通过 `setEditConfig()` 更新。

设置或修改起飞点时，地图视口使用同一 SVG 作为鼠标指针，热点位于 SVG 中心像素（11、16，浏览器指针热点采用整数像素），无需启用键盘或配置面板。指针仅作用于地图视口，确认起飞点、退出模式、清空、导入、停用、移除、容器或 View 变化及销毁后恢复原有内联指针与优先级。`draw`（航点连线）模式使用 `crosshair`。同一地图多条航线处于起飞或绘制模式时，最近进入的模式决定指针；退出后恢复仍活跃的模式指针，全部退出后恢复原指针。

### 新增航点回调

创建时传入 `onWaypointAdd(waypoint: RouteWaypoint)`，每次成功新增普通航点（地图点击打点、飞控打点（键盘、面板或 `addRouteWaypoint()`）或编辑插点）后同步调用一次。设置或修改起飞点不触发。编辑插点仅在拖动结束且去重通过后通知；修改起飞点、拖动已有点、调整高度、删除、清空、批量导入、重复位置拒绝与取消插点均不触发。

回调参数与 `exportWaypoints()` 的单项格式一致，包含 `uuid`、当前 `indexNo`、经纬度、高度及业务字段，是新的顶层对象；嵌套对象仍须只读使用。只序列化本次新增点，不导出整条航线。回调内可清空、导入或销毁控制器。此前的同步状态订阅若已移除该点或销毁控制器，则不再通知失效航点。回调抛错向调用方传播，已完成的新增不会回滚。

### 普通航点删除回调

`removeWaypoint(pointOrUuid?, options?: RouteRemoveWaypointOptions)` 仅在编辑模式删除普通航点，支持本航线 UUID 字符串或 `Feature<Point>`，省略第一个参数时删除选中点。UUID 按原值精确匹配，不存在或为空时返回 `false`，不会回退删除当前选中点。

```ts
route.setMode('edit')
route.removeWaypoint('目标航点UUID') // 默认执行删除前拦截
route.removeWaypoint('目标航点UUID', { intercept: false }) // 本次跳过删除前回调
route.removeWaypoint(undefined, { intercept: false }) // 删除选中点，本次跳过拦截
```

`options.intercept` 默认启用。设为 `false` 只跳过 `onBeforeWaypointRemove`；起飞点、当前航线归属、未提交插点和编辑模式校验仍保留，成功后照常触发 `onWaypointRemove`。此设置仅影响当前调用。

`onBeforeWaypointRemove(waypoint: RouteWaypoint)` 在 `removeWaypoint()` 通过校验后、执行删除前同步调用；`onWaypointRemove(waypoint: RouteWaypoint)` 在删除成功、点线和编辑状态更新完成后同步调用。两者仅针对普通航点，参数都是航点顶层副本，`indexNo` 保留各自通知阶段删除前的序号。嵌套业务数据应只读使用。

起飞点不能删除，尝试删除时返回 `false`，两个回调都不触发。非编辑模式、无参调用时未选中点、其他航线、已删除的点或尚未结束拖动的临时插点，以及清空航线、销毁、导入替换和取消插点也不触发。

删除前回调同步返回 `boolean | void`：只有返回 `false` 才阻止删除，返回 `true`、不返回值或未配置回调都继续删除。被阻止时 `removeWaypoint()` 返回 `false`，保留航点、选择和编辑交互，不触发 `onWaypointRemove`；回调自身造成的修改不会撤销。不支持异步返回，不等待 Promise。回调抛错会中止删除并向调用方传播；若回调清空、导入、销毁或切换模式，当前删除停止并返回 `false`。删除后回调可清空、导入或销毁；抛错向调用方传播，已完成的删除不回滚。删除过程的同步状态通知若已销毁控制器，则不再调用删除后回调。

### 状态订阅

`getState(): RouteState` 返回普通数据快照。`subscribe(listener)` 立即通知一次初始状态，之后在数据、模式、选择、输入或实时飞机变化时通知，返回取消订阅函数；`destroy()` 自动清理订阅。

飞控将同一动画帧内的位置、航向和高度更新放在一次事务中，完成后只通知一次完整状态。后续通知中某个订阅回调抛错时，通过 `console.error` 报告，不中断其他订阅或飞控循环；首次同步回传抛错仍向调用方传播，并取消本次注册。新增、删除等业务回调的异常约定不变。

普通业务按钮、输入框或下拉框切换焦点不会单独触发订阅。只有当前存在库托管的按键或指针动作时，进入表单、后台等清理才通知空动作状态；重复清理不再通知空动作。调用航线接口引起的模式、数据变化仍会正常通知，实时飞行也仍会持续通知；这不是对所有状态的全局去重。

| 字段 | 说明 |
| --- | --- |
| `mode` | 当前互斥操作模式 |
| `hasTakeoff` | 是否已有起飞点 |
| `waypointCount` | 航点数，包含起飞点，不包含临时飞机与编辑手柄 |
| `totalDistance` | 相邻航点的球面水平距离之和，单位米；少于两个点时为 0，不计高度差、临时飞机或自动返航段 |
| `selectedWaypoint` | 选中航点的普通数据，未选中为 `null` |
| `aircraft` | 操控时返回实时飞机的经纬度、`heading`、`height`、`speed`，其他模式返回末个已记录航点的飞机信息；无航点时为 `null` |
| `activeControls` | 合并后的键盘、指针动作，用于按钮高亮 |
| `editConfig` | 当前中点或吸附配置 |

实时通知只读取飞机与选中点，不逐帧导出整条航线。快照的数组、配置与航点顶层对象独立；航点嵌套业务对象与导出一样仍保留引用，应作为只读数据使用。业务表单、提示文案、权限与后端保存仍由调用方负责。

设置起飞点后立即通知 `aircraft`：位置为起飞点，初始航向和高度为 0，`speed` 为控制器配置速度（默认 10 m/s）。非操控期间跟随末个已记录航点的位置、内部航向和高度；导入时航向取 `direction`、高度取 `height`。进入操控后使用实时飞机，退出后恢复末点信息，未打点的移动不保留；清空后为 `null`。`speed` 始终为配置速度，不是实时测速，也不使用业务字段 `pointSpeed`。判断是否正在操控请使用 `mode === 'control'`，不要用 `aircraft !== null`。

调用 `setFlightConfig()` 后，`aircraft.speed` 立即反映最新水平速度，操控与非操控模式一致。有效配置变化同步通知一次订阅，空配置或值未变化不通知；无航点时仍允许配置，`aircraft` 保持 `null`。

`totalDistance` 使用 `EPSG:4326` 经纬度计算，按航线几何版本缓存，多个订阅者和飞机帧复用计算结果。新增、导入、删除、修改起飞点和编辑完成后，随状态通知更新；清空归零。拖动中主动调用 `getState()` 会读到当前临时航线长度，取消拖动后恢复。

```ts
route.subscribe(({ totalDistance }) => {
  console.log(`航线总长度：${totalDistance.toFixed(1)} 米`)
})
```

### 业务面板与输入

操作面板由业务创建、布局并根据状态控制显示。库不创建或删除业务 DOM，负责输入事件、可选激活颜色与生命周期。传入操控区和编辑区的公共容器，无需单独传编辑面板或调用绑定方法；也可以只配置 `input.keyboard: true` 使用键盘，或省略输入配置只使用地图绘制。

飞控快捷键不依赖鼠标位置或地图焦点：启用 `input.keyboard` 后，`control` 模式在地图所属 `document` 监听按键，焦点位于页面其他非输入区域时仍可操作。地图、按钮或窗口失焦不会清空已按住的动作，也不监听 `focusout` / `blur`；`document` 上的 `focusin` 仅用于进入输入框、下拉框、可编辑内容或链接时清理动作。页面进入后台、松键、指针释放或取消仍会清理对应输入。窗口失焦后若浏览器未送达 `keyup`，已按住动作会保留，直到再次按下并松开对应键、页面进入后台或退出模式。

航线输入监听仅在 `control` / `edit` 模式启用，退出时解绑并恢复临时添加的 `tabindex`；其他模式仍校验容器 ID。飞控快捷键启用时 `keydown` 绑定 `document`，编辑模式仍限定地图和业务容器；跳过已被阻止默认行为的事件，避免嵌套容器重复执行。普通业务按钮上的 WASD 等快捷键可用于飞控，Space / Enter 保留按钮原生行为。关闭快捷键时，容器内按钮仍支持 Enter / Space。`keyup` 保留 document 捕获监听；未配置 `container` 时不绑定指针事件。输入期间的松键、指针释放与页面后台清理仍保留。

```ts
const route = createRouteController({
  input: {
    keyboard: true,
    container: 'route-controls',
    activeStyle: {
      backgroundColor: '#16a34a',
      borderColor: '#22c55e',
      color: '#fff'
    }
  }
})
```

```html
<div id="route-controls">
  <button type="button" data-route-control="forward">前进</button>
  <button type="button" data-route-control="ascend">升高</button>
  <button type="button" data-route-control="waypoint">打点</button>
</div>
```

`input.container` 可传容器 ID（不带 `#`）或 `HTMLElement`。ID 优先在所属地图容器的 document 中查找，地图尚未挂载时使用当前 document；找不到时明确报错，创建失败会回收本次图层与交互。容器必须已创建，Vue 中在 `onMounted()` 后初始化，无需额外保存面板 ref。事件代理允许内部按钮动态增删。键盘 W/S/A/D 移动、Q/E 转向、Z/C 升降、空格打点；操控模式按住动作持续执行，编辑模式只处理升降，每次 1 米。高度最低 0。键盘和指针分别记录并合并，松开一个来源不会清除其他来源。输入框、选择框、可编辑内容和链接不响应快捷键；进入表单、后台、指针取消及模式切换清空动作，销毁时解绑监听和释放捕获，保留业务容器。显式启用键盘时，进入操作模式会聚焦地图容器；库临时添加的 tabindex 在解绑时恢复。未配置输入时，可直接使用 `setRouteControls()` 与 `addRouteWaypoint()` 接入其他设备。

`input.activeStyle` 可选配置按钮激活时的 `backgroundColor`、`borderColor`、`color`，值使用 CSS 颜色字符串。库根据合并后的键盘与指针动作，为容器内相同 `data-route-control` 的可用按钮临时应用内联颜色（`!important`）；释放全部输入来源、进入表单、页面后台、切换模式或销毁时恢复原有颜色及优先级，不影响其他样式。不传或省略的颜色属性仍由业务 CSS 决定。该配置只处理库托管的输入高亮，不创建面板，也不替代 `activeControls` 状态订阅或业务无障碍属性。

按钮激活期间如需动态调整其他外观，优先切换业务 CSS class，避免新增或重写包含 `var()` 的内联简写声明；这些声明无法按颜色子属性可靠合并。激活前已有的 CSS 变量简写可正常恢复。

### 按 UUID 选中航点

`selectWaypoint(uuid: string): boolean` 在 `edit` 模式下选中当前航线的普通航点，自动恢复旧航点样式、应用新航点激活样式，并同步通知一次 `subscribe()`；通知中的 `selectedWaypoint` 是最终选中航点，不会先通知空选择。`getSelectedWaypoint()` 同步返回对应 Feature。

```ts
import { Circle, Fill, Style } from 'ol/style.js'
import { createRouteController } from 'map-ol-utils'

const route = createRouteController({
  activePointStyle: new Style({
    image: new Circle({ radius: 10, fill: new Fill({ color: '#00bfff' }) })
  })
})
// 先通过 importWaypoints() 导入数据或在地图上创建航点。
route.setMode('edit')
route.selectWaypoint('waypoint-1')
```

成功返回 `true`；目标已经是唯一选中航点时仍返回 `true`，不重复通知。Shift 点击产生多个选择时，此方法只保留目标并通知。未知 UUID、起飞点、未完成插点、非编辑模式或销毁后返回 `false`，保留已有选择。此方法不自动切换模式、不移动视野、不修改航点数据。退出编辑、导入或清空等操作沿用原有选择清理规则。底层样式变更事件中重入调用返回 `false`，避免破坏原样式恢复；`subscribe()` 回调中仍可切换选中航点。

`activePointStyle` 优先于默认激活样式，支持 `Style`、`Style[]` 或样式函数，空数组隐藏选中点；未配置时使用内置橙心白边放大圆点。程序选中和鼠标选中共用此配置，取消选择恢复原航点样式。

### 航线样式与数据约定

默认航线连线颜色为橙色 `#FFA01E`。普通航点为直径 16 像素的白色外圈与半径 5 像素的橙色圆心，使用圆形样式渲染（半径 6.5、白色描边宽度 3）。未配置 `activePointStyle` 时，编辑选中航点放大为直径 20 像素的橙心白边圆点，取消选择后恢复原样式；中点和吸附插点手柄默认为白底橙边，可用 `midpointStyle` 覆盖。默认起飞点使用橙色 `#FFA01E` 的 23×32 SVG 图标，中心锚点对齐地图坐标，替换原圆点，图标内字形为白色，下方紧贴显示白色“起飞点”文字与深色描边，单独图标与飞机叠加时均保留相同的下方间距。仅在首次渲染时创建图标；起飞点同时为末点时仍叠加飞机样式。传入 `takeoffStyle` 可覆盖默认图标。

不传 `style` 时，库提供起飞点、普通点、连线、末点飞机与航向旋转的完整样式。显式传入整体 `style` 时，未配置的分类继承整体样式；`null` 隐藏整体样式，分类空数组隐藏该分类。`takeoffStyle`、`lineStyle`、`pointStyle`、`aircraftStyle` 和 `midpointStyle` 仍可分别覆盖；选中航点独立使用 `activePointStyle`，省略时采用默认激活样式。自定义飞机样式自行根据 `heading` 设置旋转。图层配置继承 `LayerConfig` 的原生参数，例如 `opacity`、`extent`、`declutter`、`renderBuffer`，显式 `className` 优先；`layerName` 在航线中可选，`map` 由控制器单独管理。

`getLayer()` 保留底层数据访问能力，但清空与替换必须使用 `clear()` 或 `importWaypoints()`，不要直接清理数据源导致内部状态失配。首点标记 `isTakeoff`，停止后末点标记 `isAircraft`；操控期间飞机由临时图层显示，不写入导出数据。航点有稳定 UUID，修改位置不改变标识。所有业务属性保存在独立 `extData` 中；高度为有限米数，允许负数，点击新增继承末点高度，操控打点使用飞机高度，插点继承前点高度，其他业务配置不自动继承。

同一航线按二维坐标精确去重，拒绝重复点击、打点或起飞点冲突时调用可选 `onDuplicate(coordinate)`，参数为坐标副本；编辑拖动重复则回滚。起飞点不能选中、拖动或删除，只能通过 `takeoff` 模式修改。起飞点与其他航点冲突时保持等待下一次点击。

点击、飞控与编辑完成时，超出 ±180° 的有限经度会归一化到该范围，并按归一化后的坐标去重；原本范围内的经度保持不变。非有限坐标或超出 ±90° 的纬度不会提交：点击忽略，编辑回滚本次拖动或插点。导入仍严格拒绝越界坐标。拖动期间仍可读取临时位置，保存应等待编辑结束；跨经线归一化保证数据可导出再导入，不拆分跨经线的显示线段。

### 导入航点

`importWaypoints(waypoints: readonly RouteWaypoint[])` 整体替换当前航线，按 `indexNo` 从小到大连线；相同序号保持输入顺序，不修改传入数组。首点设为起飞点，末点标记为飞机，无需另行点击确认。

```ts
import { createRouteController, type RouteWaypoint } from 'map-ol-utils'

const controller = createRouteController({ map })
const points: RouteWaypoint[] = [
  {
    waylineId: 'route-1',
    indexNo: 0,
    pointSpeed: 10,
    gimbalPichAngle: -45,
    direction: 90,
    shootType: 'time',
    supportLightType: 'ir,visable',
    longitude: 120.15,
    latitude: 30.28
  },
  { indexNo: 1, longitude: 120.16, latitude: 30.29 }
]
controller.importWaypoints(points)
controller.setMode('edit') // 导入后可继续编辑
// controller.importWaypoints([]) // 清空，hasTakeoff() 恢复 false
```

| 字段 | 要求 | 用途 |
| --- | --- | --- |
| `uuid` | 可选，非空白字符串，同一批导入不能重复 | 航点稳定标识；省略时自动生成 UUID v4，导出带回 |
| `indexNo` | 必填，非负整数 | 仅用于导入排序，导出时按连线顺序重新编号；不存入 Feature |
| `longitude`、`latitude` | 必填，有限数值，范围分别为 ±180、±90 | EPSG:4326 经纬度，写入 Point 几何 |
| `waylineId` | 可选，字符串类型提示，不做运行时校验 | 航线业务标识 |
| `height` | 可选，有限数值，允许负数，默认 0 | 航点高度（米），保存到 `extData.height` |
| `pointSpeed` | 可选，数值类型提示，不做运行时校验 | 速度元数据，不改变实时操控速度 |
| `gimbalPichAngle` | 可选，数值类型提示，不做运行时校验 | 云台俯仰角，保留接口原字段拼写 |
| `direction` | 可选，有限数值 | 航向角，同时写入显示用 `heading`，缺省为 0 |
| `shootType` | 可选，字符串；枚举由业务定义，不做运行时校验 | 拍照模式 |
| `supportLightType` | 可选，字符串；取值由业务定义，不做运行时校验 | 影像类型，保留接口原枚举拼写 |

经纬度只存入几何，编辑后的最新位置通过 `feature.getGeometry().getCoordinates()` 获取。`waylineId`、`pointSpeed`、`gimbalPichAngle`、`direction`、`shootType`、`supportLightType`、`height` 和其他业务属性保存在 Feature 的独立 `extData` 对象中。`uuid` 在顶层标识航点，`heading`、`isTakeoff`、`isAircraft` 保留在顶层供控制与样式使用；`indexNo` 只用于导入排序和导出重新编号，不保存在 Feature 中。新航点的 `extData` 初始化高度；点击新增继承末点高度，飞控打点取当前飞行器高度，中点插入继承前一航点高度，其他业务配置不继承。

```ts
const waypoint = controller.getSelectedWaypoint()
if (waypoint) {
  controller.updateWaypoint(waypoint, { pointSpeed: 15 })
}
```

航点标识通过 `feature.get('uuid')` 读取。导入保留已有 `uuid`，缺省时生成；点击、操控打点和中点插入各自生成新 UUID，拖动、修改起飞点、删除其他点及导出重新编号都不改变已有标识。标识在创建时确定，调用方不应直接改写。自动生成使用 Web Crypto 的 `getRandomValues()`，支持 HTTP 页面。

`RouteWaypoint` 通过 `[key: string]: unknown` 接受额外业务字段，例如 `heightMode`、`reachShoot` 或嵌套动作配置。导入除 `uuid`、`indexNo`、`longitude`、`latitude` 外的所有自有可枚举属性到 `extData`，仅校验航点结构及内部使用的 `height`、`direction`，纯业务字段不做运行时校验或模式映射。导入和导出只浅拷贝顶层，嵌套对象、数组保留引用。同名 `heading`、`isTakeoff`、`isAircraft` 等业务属性也只保存在 `extData`，不覆盖 Feature 内部标记。导出展开 `extData` 后，以控制器的 UUID、连续序号和最新坐标覆盖同名值。

业务层读取这些字段通过 `extData`，更新优先使用 `updateWaypoint()`；导出读取最新 `extData` 并生成原来的扁平结构。拖动只更新几何，不重算业务属性；修改 `extData.direction`（包括通过 `updateWaypoint()`）不自动更新样式使用的 `heading`，需要即时刷新航向显示时由调用方同时设置。

导入先校验整批航点结构、高度和航向；这些字段非法或经纬度重复时抛错，原航线、编辑选择和操控状态保持不变。成功后停止操控、取消未完成拖动、清除旧选择；非空导入保留编辑模式并重建手柄，其余模式与空数组导入回到空闲。空数组清空点线，单点只生成起飞点。销毁后调用抛错。导入不调整视野，不提供文件解析功能。

### 更新航点扩展属性

`updateWaypoint(pointOrUuid: Feature<Point> | string, properties: Partial<RouteWaypoint>): boolean` 按 UUID 或当前航线的 Feature 浅合并 `extData`。可更新起飞点和普通航点，无需切换编辑模式；不改变模式、选择、坐标、连线或内部显示标记。

```ts
const waypoint = controller.exportWaypoints()[1]
if (waypoint?.uuid) {
  controller.updateWaypoint(waypoint.uuid, {
    pointSpeed: 15,
    height: 100,
    taskName: '巡检拍照',
    actions: { shoot: true }
  })
}
```

-   成功返回 `true` 并同步通知 `subscribe()`，后续状态快照和导出包含更新值。未知 UUID、其他航线或已失效的 Feature、未完成插入的临时点、销毁后调用以及空属性对象返回 `false`，不通知。
-   `properties` 必须为非数组对象；禁止包含 `uuid`、`indexNo`、`longitude`、`latitude`，即使值为 `undefined` 也会抛错。修改位置使用地图编辑，整体替换使用 `importWaypoints()`。
-   仅对 `height`、`direction` 复用有限数值校验，`height` 允许负数；校验失败抛错，不写入任何属性。`gimbalPichAngle`、`shootType`、`supportLightType`、`pointSpeed`、`waylineId` 及其他纯业务字段不做运行时校验，由调用方决定合法值。
-   未提供的字段保留；嵌套对象或数组整体替换并保留引用，不做深合并。不会修改传入对象。`heading`、`isTakeoff`、`isAircraft` 等同名扩展字段只写入 `extData`，不会覆盖内部状态。
-   导入、更新和导出保留负高度。`adjustWaypointHeight()` 仍将结果限制到最低 0 米；飞控动作帧（包括水平移动、转向和升降）也会将临时飞机高度限制到最低 0 米，不影响直接设置已记录航点的负高度。
-   更新已记录航点不改变正在操控的临时飞机。直接 `feature.set('extData', ...)` 仍可使用，但不执行此方法的校验，也不自动通知控制器订阅。

### 批量更新全部航点

`updateAllWaypoints(properties: Partial<RouteWaypoint>): boolean` 将同一组扩展属性浅合并到全部已记录航点，包含起飞点，不包含临时飞机和尚未完成的插点。

```ts
controller.updateAllWaypoints({
  height: -20,
  pointSpeed: 15,
  shootType: 'custom-shoot',
  reachShoot: 1
})
```

校验、扩展字段透传与嵌套引用规则和 `updateWaypoint()` 相同；各航点分别合并自身已有属性，不改变 UUID、坐标、模式或选中航点。先校验全部合并结果，校验失败时整批不写入、不通知；成功写入全部目标返回 `true`，通过 `subscribe()` 通知一次最终状态。空航线、空属性对象或销毁后调用返回 `false`，不通知。

底层 Feature 的属性事件仍逐点触发。如果尚有目标待写入，而业务在这些事件中同步清空、重新导入、销毁或移除待更新航点，下一次写入前会停止旧批次并返回 `false`；已经写入的属性不回滚。业务面板优先使用控制器的 `subscribe()`，避免在底层属性监听中修改正在批量更新的航线。

### 导出航点

`exportWaypoints(): RouteWaypoint[]` 返回当前航线的数据副本，可直接用于保存或再次调用 `importWaypoints()`。

```ts
controller.importWaypoints(points)
// 绘制或编辑完成后保存
const data = controller.exportWaypoints()
const json = JSON.stringify(data)
```

-   导出包含每个航点的稳定 `uuid`，导出再导入保留标识；缺省标识的旧数据仍可导入。
-   数组包含起飞点及已记录航点，顺序与连线一致；不包含线要素、中点手柄或未打点的实时飞机。
-   `indexNo` 按当前顺序从 0 连续生成，支持插入和删除后再次导入；不向 Feature 写入序号。
-   `longitude`、`latitude` 取 Point 的当前坐标；拖动未结束时读取的是当前临时位置，持久化应在编辑完成后进行。
-   保留所有扩展业务属性，以及 `height`、`waylineId`、`pointSpeed`、`gimbalPichAngle`、`direction`、`shootType`、`supportLightType`。已声明字段未设置时，数值填 0、字符串填空字符串；扩展字段不补默认值；`direction` 优先取 `extData.direction`，缺省取绘制时记录的 `heading`，不按线段重新计算。
-   每次返回新数组和新的顶层对象，修改顶层字段不影响航线；嵌套对象和数组共享引用。不导出 Feature 自身的内部属性，`extData` 业务属性原样保留。空航线、清空和销毁后返回 `[]`，读取不会停止操控或切换编辑模式。

### 编辑与飞行配置

`RouteConfig.edit` 和 `setEditConfig(options)` 接受 `mode: 'midpoint' | 'snap'` 与 `pixelTolerance`。默认 `midpoint`、容差 10 像素；容差必须为有限正数，配置增量合并。配置失败保留当前选择和拖动；有效更新在编辑模式下取消未完成拖动、清除旧选择并重建手柄。

```ts
route.setMode('edit')
route.setEditConfig({ mode: 'snap', pixelTolerance: 15 })
route.removeWaypoint() // 删除选中普通航点；失败返回 false
route.adjustWaypointHeight(1) // 选中航点升高 1 米，最低 0
route.setMode('idle') // 完成编辑并保留结果
```

中点模式拖动固定手柄插入航点，吸附模式沿整条线定位临时点后拖动插入；悬停和单击不插点。拖动与插入会同步线坐标，重复坐标回滚。`selectWaypoint(uuid)` 可主动选中普通航点，`getSelectedWaypoint()` 读取鼠标或程序选中的底层 Feature；`removeWaypoint(pointOrUuid?, options?)` 支持按 UUID 或 Feature 删除，省略目标时操作当前选择；`adjustWaypointHeight(delta, point?)` 省略航点参数时调整当前选择。起飞点不能删除，但可以显式传入起飞点调整高度。删除全部普通点后保留起飞点并移除线。

中点模式移动已有航点时复用手柄，只更新相邻两段中点；增删航点等结构变化才重建。批量属性更新复用数据源成员索引检查剩余目标，避免逐点扫描整条航线；仍保留同步回调导致目标失效时中断旧批次的保护。

创建配置的 `speed`、`turnSpeed`、`verticalSpeed` 默认分别为 10 m/s、60°/s、1 m/s，均为有限正数。新启动飞控从最后确认的航点及其高度开始，机头朝北；重复进入 `control` 保持当前飞机状态。按球面距离计算位移，移动时视野跟随，手动平移不修改飞机位置；单独转向、升降和打点不改变地图中心。动画帧停顿最多计入 100ms。编辑单次高度步长不受飞控速度影响。临时飞机不进入导出结果，持久化拖动结果应等待拖动结束。

### 动态更新飞行速度

`setFlightConfig(options: RouteFlightConfig): void` 可在任意未销毁状态增量更新飞行参数，包括尚无起飞点、正在操控和编辑模式。

```ts
route.setFlightConfig({ speed: 15 }) // 水平速度 15 米/秒，其余参数保留
route.setFlightConfig({
  speed: 20,
  turnSpeed: 90, // 转向速度，度/秒
  verticalSpeed: 2 // 升降速度，米/秒
})
console.log(route.getState().aircraft?.speed) // 已有航点时为 20
```

-   省略或传入 `undefined` 的字段保留当前值；不会保留传入对象的引用。原有创建参数仍位于 `RouteConfig` 顶层。
-   参数必须为非数组对象，速度必须为有限正数；`0`、负数、`NaN`、无穷值和字符串均抛错。整批校验成功后才写入，失败保留原配置和飞行状态，不通知。
-   正在操控时，后续动画帧使用新参数；不重启会话、不移动飞机、不重置航向、高度、按键或面板动作，也不取消编辑选择。停止运动仍使用输入释放或 `setRouteControls([])`。
-   更新后的配置在模式切换、清空、导入航点和重新进入操控后保留；销毁后调用抛错。不会修改已记录航点的 `pointSpeed` 或导出数据；`updateWaypoint()` 和 `updateAllWaypoints()` 也不会改变飞行参数。
-   `getState()` / `subscribe()` 的 `aircraft.speed` 和供自定义飞机样式读取的 Feature `speed` 同步更新；一次有效配置变化只通知一次，空配置或等值更新不通知。

* * *

## 十一、类型定义（`core/types.ts`，`export * from './core/types'`）

```ts
interface CustomMap extends OlMap {
  activeBaseLayer?: string
  changeBaseLayer?: () => void
  getLayerByName?: (name: string) => Layer<Source> | undefined
  removeLayerByName?: (name: string) => void
  changeLayersByNames?: (activeNames: string[], allLayerNames: string[]) => void
}

type MassData = {
  _uuid?: string
  id: string
  style: number
  name: string
  status: string
  width?: number
  height?: number
  crsForm?: CRSTypes
  rawData: Record<string, any>
  lnglat: [string, string] | [number, number]
}

enum GeometryType {
  LineString = 'LineString',
  MultiLineString = 'MultiLineString',
  Polygon = 'Polygon',
  MultiPolygon = 'MultiPolygon',
  Invalid = 'Invalid'
}
```

完整导出还包括：`VectorConfig`、`ClusterConfig`、`IconConfig`、`WmtsSourceOptions`、`GetImgUrl`、`GetImgUrlFn`、`HzsyType`、`ZheJiangType`、`WorldLayerType`、`WorldLayerLocalOptions`、`GeometryCoordinates`、`RouteConfig`、`RouteEditConfig`、`RouteFlightConfig`、`RouteWaypoint`、`RouteControl`。

* * *

## 图层约定属性

工具库通过图层 properties 标记元信息，维护时请保持一致：`_isBaseLayer`、`_layerName`、`_isCustom`、`_isVectorLayer`、`_isClusterLayer`、`_isMassMarksLayer`、`_selectInteraction`、`_resolutionChangeKey`、`_uuidSet`、`_dataList`、`_vectorConfig`、`_isAutoFit`、`_massMarksStyles`、`_statusNum`。

## 构建

构建保留 ESM 与 CommonJS 桥接入口；`checks.legacyCjs: false` 仅关闭 tsdown 的 CommonJS 格式迁移建议，其他构建警告仍正常输出。

```bash
pnpm --dir packages/map-utils build       # tsdown 产出 esm + cjs + d.ts（压缩）
pnpm --dir packages/map-utils check:types # tsc --noEmit
pnpm --dir packages/map-utils test        # build + check:types + node:test
pnpm --dir packages/map-utils dev         # watch 模式
```

新增回归位于 `test/data-regressions.test.ts`、`test/lifecycle-and-worker.test.ts`；示例包 `map-utils-layers.spec.ts` 在 Chromium 中验证嵌套图层移除、双地图隔离及千点连续缩放只跨档更新样式。

## 注意事项

-   **地图归属**：省略可选地图参数时先创建单例；多地图调用显式传入所属地图。中心按地图保存，销毁独立地图不清空单例。`emitter` 仍是包级共享事件总线，业务订阅需按数据区分来源并自行解绑。
-   **坐标系**：输入数据可通过 `config.crsForm` / `data.crsForm` 指定源坐标系，内部统一转为 `WGS84`。
-   **天地图令牌**：`createMapWorldLayer` 在线模式与 `createZheJiangLayer` 内置了默认 `tk`，正式项目请传入自有令牌；`createMapWorldLayer` 第三个 `config` 参数的本地 `{ url }` 模式无需令牌。

## 常见问题

### map-ol-utils 是什么？

OpenLayers实用程序与浏览器端天地图私有VTS的加载和转换

### 如何安装 map-ol-utils？

在项目目录运行 npm install map-ol-utils，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：ol（^10\.10\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### map-ol-utils 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### map-ol-utils 支持 ESM 还是 CommonJS？

同时提供 ES Module 与 CommonJS 入口，import 和 require\(\) 都可以使用，打包工具会自动选择合适的版本。

### map-ol-utils 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js ^22\.18\.0 \|\| ^24\.0\.0 \|\| \>=26\.0\.0。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### map-ol-utils 可以商用吗？

map-ol-utils 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### map-ol-utils 还在维护吗？

最新版本 1\.2\.10 发布于 2026/11/04（今天）；至今共发布 129 个版本，其中过去 12 个月发布了 30 个；首次发布于 2025/04/10。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

