npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

map-ol-utils

v1.1.7

Published

OpenLayers utilities with browser-side Tianditu private VTS loading and conversion

Readme

map-ol-utils

目录:packages/map-utils | npm 包名:map-ol-utils

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

特性

  • 单例地图实例:模块内维护一个全局 instance,多数工具函数直接操作该单例,无需层层传参。
  • 多种底图:杭州规自瓦片、全国天地图(WMTS)、浙江省天地图(XYZ)。
  • 业务矢量图层:根据业务数据自动推断几何类型(线/面)并生成 GeoJSON 图层,支持着色映射、点击高亮。
  • 海量点 MassMarks:基于状态值的多套样式(默认 + 激活),可附带伴生线/面图层。
  • 聚合图层:基于 ol/source/Cluster,半径/颜色随聚合数量动态计算。
  • 坐标系转换:内部统一通过 gcoord 转换到 WGS84(EPSG:4326)。
  • 事件总线:内置 mitt 实例 emitter,用于派发要素选中等事件。
  • 扩展能力:提供天地图组合、栅格着色、标记、绘制量算、GeoJSON 图层和多图层选择能力。
  • 私有 VTS 转换:通过 tdt-vts-core 复用 PK 编解码、私有 Protobuf/MVT 转换和 Web Worker 解码,本包只保留 OpenLayers 适配。

项目结构

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,内部模块不作为公共入口。

安装

pnpm add map-ol-utils ol gcoord

ol 是对等依赖(要求 ^10.10.0),需由宿主项目安装。tdt-vts-core 是普通依赖,由包管理器自动安装。

快速开始

import { createMapInstance, createBusinessLayer, 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)

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

天地图私有 VTS

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

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() 返回独立地图,其挂载的图层方法只操作自身。

一、地图实例(map/create-map.ts)

createMapInstance(config?)

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

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

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

返回:CustomMap(OpenLayers Map 的扩展,附带 getLayerByName / removeLayerByName / changeLayersByNames 方法与 activeBaseLayer 字段)。

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

createMap(config?)

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

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

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 标记当前底图名 |

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

getMapInstance()

function getMapInstance(): CustomMap

返回当前单例(等价于导出的 instance 变量)。

disposeMap()

function disposeMap(): void

销毁单例:移除全部覆盖物、清空交互、移除并释放(source.dispose() + layer.dispose())所有图层,setTarget('') 后将 instance 置为 undefined。未初始化或重复调用时会安全跳过。

resetMap(config?)

function resetMap(config?: { center?: Coordinate; isClearAll?: boolean }): void
// 默认:{ center: 上次中心, isClearAll: true }

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

resetZoomAndCenter(center?)

function resetZoomAndCenter(center?: [number, number]): void // 默认上次中心

动画(1s)复位到默认缩放 9.4 与指定中心。

instance

let instance: CustomMap // 导出变量;未创建时为 undefined

二、底图与图层名管理(layer/base-layer.ts)

createBaseLayer(layerName?)

杭州规自瓦片底图(EPSG:4326,XYZ,5 路负载)。

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

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

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

createMapWorldLayer(layerName, tk?)

全国天地图 WMTS 图层。

function createMapWorldLayer(layerName: WorldLayerType, tk?: string): Tile
// WorldLayerType = 'vec_c' | 'cva_c' | 'img_c' | 'cia_c' | 'ter_c' | 'cta_c' | 'ibo_c'

| 参数 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | layerName | WorldLayerType | — | 形如 img_c,内部按 _ 拆分为图层名与 matrixSet | | tk | string | 内置默认令牌 | 天地图访问令牌,建议自行传入 |

map.addLayer(createMapWorldLayer('img_c', '你的天地图令牌'))

createZheJiangLayer(layerName?, tk?)

浙江省天地图 XYZ 图层。

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

getLayerByName(name)

function getLayerByName(name: string): Layer<Source> | undefined

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

removeLayerByName(name)

function removeLayerByName(name: string): void

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

changeLayersByNames(activeNames, allLayerNames)

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

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

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

三、矢量图层(layer/vector-layer.ts)

createBusinessLayer(dataList, config)

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

function createBusinessLayer(
  dataList: MassData[],
  config: VectorConfig
): VectorLayer

| config 关键字段 | 类型 | 说明 | | --- | --- | --- | | key | string | 数据中存放坐标数组的字段名 | | isLine | boolean | 是否线图层(影响是否生成填充样式) | | layerName | string? | 图层名(className) | | 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。

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

createVectorLineLayer(dataList?, config) / createVectorFaceLayer(dataList?, config)

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

function createVectorLineLayer(
  dataList?: MassData[],
  config: VectorConfig
): VectorLayer
function createVectorFaceLayer(
  dataList?: MassData[],
  config: VectorConfig
): VectorLayer

appendFeaturesByLayer(targetLayer, dataList, crsForm?)

向已有矢量图层追加要素(同步更新其 _uuidSet),复用图层 _vectorConfig 生成样式;未传 crsForm 时继承图层配置的源坐标系。

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

四、海量点 MassMarks(layer/mass-marks.ts)

buildMassMarksStyles(getImgUrlFn)

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

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)拼接。

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

createMassMarksLayer(dataList, styleList?)

function createMassMarksLayer(
  dataList: MassData[],
  styleList?: StyleLike[]
): VectorLayer

按 item.style 索引在 styleList 中取样式创建要素图层(zIndex: 2000)。图层附带 _isMassMarksLayer、_statusNum、_dataList、_uuidSet 等属性。

addMassMarksLayer(getImgUrlFn)

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

function addMassMarksLayer(
  getImgUrlFn: GetImgUrlFn
): (
  dataList?: MassData[],
  iconConfig: IconConfig,
  openClick?: boolean
) => 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[]? | 直接指定样式(跳过自动生成) | | vectorConfig | VectorConfig? | 伴生线/面图层配置覆盖 |

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

addFeaturesByLayer(targetLayer, dataList, config)

向海量点图层增量添加要素(复用图层 _massMarksStyles)。

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

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

updateMarksLayersByStatus(checkedStatusList?)

function updateMarksLayersByStatus(checkedStatusList?: string[]): MassData[]

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

changeFeaturesVisibleByIds(layerName, visibleIdSet)

function changeFeaturesVisibleByIds(
  layerName: string,
  visibleIdSet: Set<string>
): void

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


五、聚合图层(layer/cluster-layer.ts)

createClusterLayer(dataList, config)

function createClusterLayer(
  dataList: ClusterData[],
  config: ClusterConfig
): VectorLayer

type ClusterData = {
  _uuid: string
  name: string
  style: number
  lnglat: number[]
  rawData: Record<string, any>
}
type ClusterConfig = {
  distance: number
  minDistance: number
  singleStyles: Style[]
  layerName?: string
  openClick?: boolean
  computedRadius?: (size: number) => number
  buildStyle?: (feature: FeatureLike) => Style
}

未传 buildStyle 时使用内置样式:聚合点半径与色相(hsla)随聚合数量变化;size === 1 时使用 singleStyles。

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

六、交互与选中(interaction/select.ts)

createSelectByLayer(vectorLayer)

为指定图层创建 ol/interaction/Select(单击、multi: false、hitTolerance: 10),命中要素时切换激活样式并派发 selected-feature 事件,支持聚合图层单点展开。交互会写入图层属性 _selectInteraction。

function createSelectByLayer(vectorLayer: VectorLayer): Select

activeFeatureByUid(uid)

function activeFeatureByUid(uid: string): void

在带 _isVectorLayer 且其 _uuidSet 含该 uid 的图层中,程序化触发对应要素的选中(派发 select 事件并加入选中集合)。

resetActiveFeature() / resetActiveVector()

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

七、覆盖物与视野(overlay/overlay.ts、map/view-fit.ts)

createOverlay(config?)

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

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

removeOverlays(key?) / removeAllLayers()

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

createAnimationMarker(imgUrl, data)

function createAnimationMarker(imgUrl: string, data: MassData): void

以 data.lnglat(经 data.crsForm → WGS84 转换)创建动画标注覆盖物,先移除已有动画标注。data 中读取 width / height / lnglat / crsForm。

setFitViewByCoords / setFitViewByGeom / setFitViewByLayer

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 自适应

三者默认 padding: [50,50,50,50]、duration: 1000,可由 fitOptions 覆盖。

setFitViewByCoords([
  [120.1, 30.2],
  [120.3, 30.4]
])
setFitViewByLayer(layer, undefined, { duration: 500 })

getLayersDataList()

function getLayersDataList(): any[]

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


八、WMTS(layer/ogc-utils.ts)

createWmtsLayer(serverUrl)

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

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'
}
const build = createWmtsLayer('https://geoserver.example.com/geoserver')
const layer = await build({ layerName: 'ws:store', matrixSet: 'EPSG:4326' })
map.addLayer(layer)

九、事件总线(emitter)

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

| 事件名 | 载荷 | 触发时机 | | --- | --- | --- | | selected-feature | 要素属性对象 | 要素被选中(createSelectByLayer 内触发) | | dynamic-marker | 动画标注配置 | 选中动态图层(_isDynamic)要素 1s 后 |


十、扩展能力

以下 API 补充天地图组合、标记、绘制与 GeoJSON 等能力;createMapInstance、createOverlay、disposeMap 等既有 API 的签名、默认值和行为保持不变。disposeInstance() 直接复用 disposeMap() 的完整清理语义。

天地图组合与栅格处理

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 个线程。

标记与选择

const markerLayer = createMarkersLayer(markerData, {
  layerName: 'device-layer',
  zIndex: 10
})
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):按缩放级别切换 img、smallImg、largeImg,通过库的生命周期方法移除时自动解绑缩放监听。
  • changeUnselectStyle(featureCollection):清空选择集合并恢复未选中样式。
  • createLayerSelectByNames(layerNames, selectedHandler?, unSelectHandler?):按多个图层名选择标记。
  • createMoveMarkerLayer(dataItem, config):创建单标记图层并加入当前地图。

绘制、GeoJSON 与视野

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

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

航线控制器

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

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.setMode('edit')    // 编辑已有航点
// 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,不会回退删除当前选中点。

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() 自动清理订阅。

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

| 字段 | 说明 | | --- | --- | | 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。

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

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 时不绑定指针事件。输入期间的松键、指针释放与页面后台清理仍保留。

const route = createRouteController({
  input: {
    keyboard: true,
    container: 'route-controls',
    activeStyle: {
      backgroundColor: '#16a34a',
      borderColor: '#22c55e',
      color: '#fff'
    }
  }
})
<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 变量简写可正常恢复。

航线样式与数据约定

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

不传 style 时,库提供起飞点、普通点、连线、末点飞机与航向旋转的完整样式。显式传入整体 style 时,未配置的分类继承整体样式;null 隐藏整体样式,分类空数组隐藏该分类。takeoffStyle、lineStyle、pointStyle、aircraftStyle 和 midpointStyle 仍可分别覆盖。自定义飞机样式自行根据 heading 设置旋转。图层配置支持 layerName、zIndex、visible、minZoom 与 maxZoom。

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

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

导入航点

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

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 | 可选,''、time、distance、none | 拍照模式 | | supportLightType | 可选,空字符串或 ir、visable 的逗号组合 | 影像类型,保留接口原枚举拼写 |

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

const waypoint = controller.getSelectedWaypoint()
if (waypoint) {
  const extData = waypoint.get('extData')
  waypoint.set('extData', { ...extData, pointSpeed: 15 })
}

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

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

业务层读取或更新这些字段应通过 extData;导出读取最新 extData 并生成原来的扁平结构。拖动只更新几何,不重算业务属性;修改 extData.direction 不自动更新样式使用的 heading,需要即时刷新航向显示时由调用方同时设置。

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

导出航点

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

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 像素;容差必须为有限正数,配置增量合并。配置失败保留当前选择和拖动;有效更新在编辑模式下取消未完成拖动、清除旧选择并重建手柄。

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

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

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


十一、类型定义(core/types.ts,export * from './core/types')

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、ClusterData、IconConfig、WmtsSourceOptions、GetImgUrl、GetImgUrlFn、HzsyType、ZheJiangType、WorldLayerType、GeometryCoordinates、RouteConfig、RouteEditConfig、RouteWaypoint、RouteControl。


图层约定属性

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

构建

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 模式

注意事项

  • 单例副作用:绝大多数独立函数依赖模块级 instance,请先调用 createMapInstance;createMap 只创建独立地图,disposeMap 后 instance 为 undefined。
  • 坐标系:输入数据可通过 config.crsForm / data.crsForm 指定源坐标系,内部统一转为 WGS84。
  • 天地图令牌:createMapWorldLayer / createZheJiangLayer 内置了默认 tk,正式项目请传入自有令牌。