map-ol-utils
v1.1.7
Published
OpenLayers utilities with browser-side Tianditu private VTS loading and conversion
Maintainers
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 gcoordol 是对等依赖(要求 ^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() // 同时释放解码 Workerhosts 与 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
): VectorLayerappendFeaturesByLayer(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 }
): voidconfig.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): SelectactiveFeatureByUid(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)图层 + overlaycreateAnimationMarker(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,正式项目请传入自有令牌。
