maplibre-utils
v1.0.0
Published
MapLibre GL JS utilities with Tianditu private VTS support
Maintainers
Readme
maplibre-utils
基于 MapLibre GL JS 6.6 的地图工具包,提供地图实例管理、GeoJSON、栅格瓦片、业务矢量、标记、交互、视图定位,以及天地图私有矢量瓦片(TDT VTS)支持。
公共函数名称和常用图层调用尽量与 map-ol-utils 保持一致,方便业务代码迁移;底层参数、返回值和渲染语义仍使用 MapLibre 原生能力。
环境要求
- 现代浏览器,支持 WebGL、Worker、AbortController 和 structuredClone
- MapLibre GL JS 版本:
^6.6.0 - TypeScript 项目可直接使用包内声明文件
本包面向浏览器运行。天地图私有矢量瓦片会在 Worker 中解码。
安装
pnpm add maplibre-utils maplibre-gl应用入口需要引入 MapLibre 样式:
import 'maplibre-gl/dist/maplibre-gl.css'地图容器必须有明确高度:
#map {
width: 100%;
height: 100vh;
}快速开始
import 'maplibre-gl/dist/maplibre-gl.css'
import {
createGeoJsonLayer,
createMapInstance,
disposeMap
} from 'maplibre-utils'
const map = createMapInstance({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [116.391, 39.907],
zoom: 10
})
map.once('load', () => {
createGeoJsonLayer(
{
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: { name: '北京' },
geometry: {
type: 'Point',
coordinates: [116.391, 39.907]
}
}
]
},
null,
{ layerName: 'cities' }
)
})
// Vue onUnmounted、React useEffect cleanup 等卸载阶段调用
disposeMap()坐标约定
对外坐标统一使用 MapLibre 的 [longitude, latitude],即 [经度, 纬度],通常是 WGS84 经纬度:
const hangzhou: [number, number] = [120.1551, 30.2741]业务代码不需要把坐标预先转换为 EPSG:3857。MapLibre 会在内部将经纬度投影到 Web Mercator 完成渲染。
地图实例与生命周期
createMapInstance(config?)
创建并保存全局单例。大多数工具函数在没有显式传入 map 时使用该单例。重复调用会返回已存在的实例,不会重新创建地图。
const map = createMapInstance({
target: 'map', // container 的兼容别名
center: [120.1551, 30.2741],
zoom: 11,
minZoom: 3,
maxZoom: 18,
style: { version: 8, sources: {}, layers: [] },
baseLayer: 'business-base'
})除 target 和 baseLayer 外,其余配置透传给 MapLibre MapOptions。
默认值:
| 配置 | 默认值 |
| ---------------------- | ------------------------------------------ |
| container / target | 'map' |
| center | [119.71512016971076, 30.122490541963536] |
| zoom | 9.4 |
| minZoom | 0 |
| maxZoom | 18 |
| style | 空的 Style Specification v8 |
| attributionControl | false |
createMap(config?)
创建独立的 MapLibre Map,但不写入全局单例。需要把该实例通过支持的 config.map 参数传给图层函数;需要依赖全局实例的函数应配合 createMapInstance 使用。
查询与销毁
import {
disposeInstance,
disposeMap,
getMapInstance,
resetMap,
resetZoomAndCenter
} from 'maplibre-utils'
const map = getMapInstance()
resetZoomAndCenter([120.1551, 30.2741])
resetMap({ center: [120.1551, 30.2741], isClearAll: true })
disposeMap()
// disposeInstance 是 disposeMap 的兼容别名resetZoomAndCenter:以 1 秒动画回到指定中心和缩放级别9.4。resetMap:默认先销毁所有已注册图层,再重置视图;isClearAll: false可保留图层。disposeMap:销毁已注册图层、移除 MapLibre 实例并清空单例。
图层句柄
本包创建的业务图层返回 LayerHandle:
interface LayerHandle {
name: string
sourceId?: string
layerIds: string[]
data?: GeoJSON
get(key: string): unknown
getClassName(): string
getProperties(): Record<string, unknown>
getSource(): MapLibreSource | undefined
getVisible(): boolean
set(key: string, value: unknown): void
setProperties(properties: Record<string, unknown>): void
setVisible(visible: boolean): void
dispose(): void
}一个业务图层可能由一个数据源和多个 MapLibre 样式图层组成,因此统一通过句柄管理:
const layer = createGeoJsonLayer(data, null, { layerName: 'business' })
layer.setVisible(false)
layer.setVisible(true)
layer.dispose()create*Layer() 已自动把图层加入 MapLibre 并登记句柄。迁移原有“创建后再加入地图”的业务代码时使用 registerLayer;重复登记同一个句柄是安全的:
const layer = createGeoJsonLayer(data, null, { layerName: 'business' })
map.registerLayer(layer) // 也可使用 registerLayer(layer, map)
map.removeLayerByName('business')MapLibre 原生 map.addLayer(layerSpec, beforeId?) 和 map.removeLayer(id) 的签名与行为保持不变。
CustomMap 保持为可接收原生 MapLibre Map 的宽输入类型;createMap()、createMapInstance()、getMapInstance() 和 instance 使用 CreatedMap,其业务兼容方法均为必填:
map.getAllLayers()
map.addInteraction(interaction)
map.removeInteraction(interaction)
map.addOverlay(overlay)
map.removeOverlay(overlay)同一地图内 layerName 必须唯一。dispose() 可重复调用,并会移除对应样式图层、数据源和内部注册信息。
全局图层管理:
getLayerByName('business')
changeLayer(layer, false)
changeLayersByNames(['roads'], ['roads', 'buildings'])
getLayersDataList()
removeLayerByName('business')
removeAllLayers()getLayersDataList() 与 map-utils 一样返回业务图层保存的原始数据并扁平化;普通 GeoJSON 图层没有业务 dataList 时不会出现在结果中。removeAllLayers() 保留通过内置底图函数创建的底图句柄。
GeoJSON 数据图层
默认样式
const layer = createGeoJsonLayer(geoJson, null, {
layerName: 'business',
visible: true,
beforeId: 'labels'
})不传 style 或传 null 时,内部按几何类型创建默认图层:
- Polygon:蓝色半透明填充
- LineString / Polygon:红色线条
- Point:蓝色圆点和白色描边
自定义 MapLibre 样式
const layer = createGeoJsonLayer(
geoJson,
{
fill: {
type: 'fill',
paint: {
'fill-color': '#1677ff',
'fill-opacity': 0.35
}
},
line: {
type: 'line',
paint: {
'line-color': '#0958d9',
'line-width': 2
}
},
point: {
type: 'circle',
paint: {
'circle-radius': 5,
'circle-color': '#52c41a'
}
}
},
{
layerName: 'custom-geojson',
source: { generateId: true }
}
)样式对象使用 MapLibre LayerSpecification,无需填写 id 和 source,本包会生成并绑定。
更新数据
changeSource(layer, nextGeoJson)changeSource 仅适用于带 GeoJSON 数据源的句柄,同时更新 MapLibre 数据源与 layer.data。
栅格与 WMTS
createWmtsLayer 同时支持直接瓦片模板和 GeoServer WMTS GetCapabilities:
const createImageryLayer = createWmtsLayer(
'https://tiles.example.com/{z}/{x}/{y}.png'
)
const imagery = await createImageryLayer({
layerName: 'imagery',
tiles: [
'https://tiles-0.example.com/{z}/{x}/{y}.png',
'https://tiles-1.example.com/{z}/{x}/{y}.png'
],
tileSize: 256,
opacity: 0.8,
attribution: '© Example'
})tiles 优先于 url。如果没有提供二者且 serverUrl 不是 {z}/{x}/{y} 模板,layerName 应使用 workspace:layer;本包会请求与 map-utils 相同的 GeoWebCache GetCapabilities 地址,优先使用 ResourceURL,否则根据 style、format、version 和 matrixSet 生成 GetTile 地址。成功的 Capabilities 请求会按 URL 缓存;失败请求会从缓存移除,后续调用可重试。
内置栅格地址
const definitions = baseLayers(tiandituToken)
const world = createMapWorldLayer('img_c', tiandituToken)
const hz = createBaseLayer('hzsyvector_dark')
const zj = createZheJiangLayer('emap')
// 正式环境可覆盖包内与 map-utils 一致的默认 Token
const zjWithToken = createZheJiangLayer('emap', zhejiangToken)baseLayers(tk)返回天地图影像、影像注记、矢量和矢量注记的 URL 定义,不直接添加图层。createMapWorldLayer支持vec_c、cva_c、img_c、cia_c、ter_c、cta_c、ibo_c。createBaseLayer使用杭州城市大脑固定服务地址。createZheJiangLayer使用浙江政务服务固定 WMTS 地址,默认 Token 与map-utils一致。该服务使用EPSG:4326XYZ 矩阵,本包通过内部协议请求正确的源瓦片行,并重投影为 MapLibre 需要的 Web Mercator 栅格瓦片;业务代码无需自行换算{x}/{y}。
业务线面与聚合图层
线和面
assembleGeoJson 从业务字段读取坐标数组或 JSON 字符串,并生成 GeoJSON FeatureCollection:
const data = [
{
id: 'road-1',
name: '道路',
status: 'normal',
style: 0,
lnglat: [120.1, 30.2] as [number, number],
rawData: {},
path: [
[120.1, 30.2],
[120.2, 30.3]
]
}
]
const lineLayer = createVectorLineLayer(data, {
key: 'path',
isLine: true,
layerName: 'roads',
strokeColor: '#1677ff',
strokeWeight: 3
})
appendFeaturesByLayer(lineLayer, moreData)可用函数:
assembleGeoJson(dataList, config):只组装数据,不创建图层。createBusinessLayer(dataList, config):根据isLine创建线或面图层。createVectorLineLayer(dataList, config):强制创建线图层。createVectorFaceLayer(dataList, config):强制创建面图层。appendFeaturesByLayer(layer, dataList, crsForm?):复用图层创建时保存的VectorConfig,向现有 FeatureCollection 追加要素。
无法解析或维度不匹配的坐标会被跳过。
点聚合
const cluster = createClusterLayer(
[
{
_uuid: 'point-1',
name: '站点',
style: 0,
lnglat: [120.1551, 30.2741],
rawData: {}
}
],
{
layerName: 'stations',
distance: 50
}
)该函数创建聚合圆、聚合数量和非聚合点三个 style layers。distance 对应 MapLibre clusterRadius。
标记图层
单个标记
const marker = createMarker(
{
lng: 120.1551,
lat: 30.2741,
img: '/images/marker.webp',
activeImg: '/images/marker-active.webp',
text: '站点 A',
showText: true,
scale: 1,
rotation: 0
},
{ layerName: 'stations' }
)
marker.addTo(map)createMarker 返回原生 maplibregl.Marker,不会自动添加到地图。
标记集合
const markers = createMarkersLayer(dataList, {
layerName: 'stations',
visible: true
})
markers.markers[0].setLngLat([120.2, 30.3])
markers.setVisible(false)
markers.dispose()createMarkersLayer 会立即将标记添加到地图,并返回带 markers 数组的 MarkersLayerHandle;同一地图内 layerName 必须唯一,重复创建会立即抛错。
其他标记接口:
createIconStyle(item, active?):创建原生MarkerOptions。createText(item):创建文字 DOM。createElasticMarkerLayer(dataList, config):缩放小于 11 使用img,11–13 使用smallImg,14 及以上使用largeImg。createMoveMarkerLayer(item, config):创建单个可移动标记,并同时返回其图层句柄。changeUnselectStyle(markers):移除标记 DOM 的data-selected属性并清空传入数组。
海量点兼容 API
const massLayer = createMassMarksLayer(dataList, [], {
layerName: 'mass-points'
})
addFeaturesByLayer(massLayer, moreData)
addFeaturesByLayer(massLayer, replacementData, { clear: true })传入 styleList 时使用 MapLibre 符号图层渲染图标,样式数组前半段是默认态、后半段是激活态;图片通过 map.loadImage() 注册。不传样式时回退为 GeoJSON 圆形图层。dispose() 会同时移除该图层注册的图片;异步图片在销毁后完成加载时不会重新写入地图。
addMassMarksLayer 同样支持第三个 openClick 参数,以及 hasLine、hasFace、marksStyles、offset 和 vectorConfig。其返回句柄会统一拥有选择交互及可选的线、面关联图层:创建中途失败会回滚,调用 dispose() 会一并清理。
updateMarksLayersByStatus(statusList) 会给海量点样式图层设置状态过滤条件,同时返回状态命中的业务数据。
选择交互与要素状态
点击图层
const interaction = createSelectByLayer(layer, (feature) => {
console.log(feature.get('data'), feature.properties)
})
interaction.dispose()按图层名称绑定:
const interaction = createLayerSelectByNames(['roads', 'stations'], (feature) =>
console.log(feature)
)返回的 SelectInteraction.dispose() 会解绑点击事件并清理当前选择。回调参数保留 GeoJSON 字段,并提供 feature.get() / feature.getProperties() 兼容方法。通过 createSelectByLayer() 创建的交互绑定到传入图层及其地图,销毁图层时会自动销毁交互。选择和取消选择会更新 feature-state.active,unSelectHandler 会在切换选择、点击空白区域或销毁交互时调用。标记图层使用相同函数时会切换 activeImg。
激活要素
activeFeatureByUid('feature-1', layer.sourceId)
resetActiveFeature()
resetActiveVector() // 同时移除当前地图的 Overlay激活状态按地图实例隔离;这些函数可显式传入地图。resetActiveVector(map) 还会移除该地图上的覆盖物,不影响其他地图实例。
activeFeatureByUid 调用 MapLibre setFeatureState 设置 { active: true }。要看到视觉变化,图层样式需要读取 feature-state:
const pointStyle = {
type: 'circle' as const,
paint: {
'circle-color': [
'case',
['boolean', ['feature-state', 'active'], false],
'#ff4d4f',
'#1677ff'
]
}
}GeoJSON 要素必须有稳定的 id,或者数据源开启 generateId: true。
绘制控件
本包不绑定具体 MapLibre Draw 实现,也不额外安装绘制依赖:
const control = createDrawInteraction(drawControl)
map.addControl(control)
const styles = getDrawStyle()createDrawInteraction(control)原样返回实现了 MapLibreIControl的控件。getDrawStyle()返回基础线、面绘制样式,可传给兼容的 Draw 库。
视图定位
setFitViewByCoords(
[
[120.1, 30.2],
[120.4, 30.5]
],
map,
{ padding: 80, duration: 500 }
)
setFitViewByGeom(geometry, map)
setFitViewByLayer(layer, map)
flyToAnimate([120.1551, 30.2741], 13, map)- 空坐标数组不执行操作。
- 单个坐标使用
easeTo,默认缩放级别为14。 - 多个坐标计算边界并使用
fitBounds,默认padding: 50、duration: 1000。 setFitViewByGeom支持带coordinates的 GeoJSON Geometry;GeometryCollection 当前不会收集子几何。setFitViewByLayer从句柄保存的 GeoJSON 数据计算范围。
覆盖物与动画标记
const element = document.createElement('div')
element.textContent = '详情'
const popup = createOverlay(
element,
[120.1551, 30.2741],
'bottom',
'station-detail'
)
popup.setPosition([120.16, 30.28])
const marker = createAnimationMarker('/images/pulse.gif', data)
removeOverlays('station-detail')
removeOverlays() // 移除当前地图上的全部已注册 Overlay 和动画 MarkercreateOverlay 返回带 setPosition() 兼容方法的原生 maplibregl.Popup;createAnimationMarker 返回原生 maplibregl.Marker。
天地图私有矢量瓦片
直接创建完整图层
const tdtLayer = await createTdtVtsLayer({
id: 'tdt-vector',
token: 'YOUR_TDT_TOKEN',
hosts: ['https://host-0.example.com', 'https://host-1.example.com'],
style: '/styles/tdt-style.json',
minZoom: 1,
maxZoom: 18,
visible: true,
diagnostics: {
onDecoded(stats, url) {
console.debug('TDT tile decoded', stats, url)
},
onError(error, url) {
console.error('TDT tile failed', url, error)
}
}
})
tdtLayer.setVisible(false)
tdtLayer.dispose()配置说明:
| 字段 | 必填 | 说明 |
| ---------------- | ---- | -------------------------------------------- |
| token | 否 | 可选访问令牌;空值时不生成 tk 查询参数 |
| hosts | 是 | 非空服务地址数组,按瓦片坐标稳定分配服务地址 |
| style | 是 | Style v8 JSON 地址或已合并完成的样式对象 |
| id | 否 | 数据源、图层命名前缀,默认 tdt-vts |
| minZoom | 否 | 数据源最小级别,默认 1 |
| maxZoom | 否 | 数据源最大级别,默认 18 |
| beforeId | 否 | 新增样式图层的插入位置 |
| visible | 否 | 传 false 时初始隐藏全部 TDT style layers |
| map | 否 | 指定 MapLibre 实例,否则使用全局单例 |
| diagnostics | 否 | 解码成功与失败回调,URL 中的 token 会被脱敏 |
| decoderOptions | 否 | Worker 工厂与请求超时配置 |
hosts 和 style 都是必填项:
type Hosts = readonly [string, ...string[]]传入地址时,本包会请求并校验 Style v8 JSON;服务端需允许当前页面跨域访问。也可以直接传入已加载、已合并的样式对象。天地图图层使用的数据源标识为 base-tdt-vector-tile。
处理流程:
MapLibre 请求 tdt-vts:// 瓦片
→ 根据 hosts 生成天地图真实地址
→ Worker 请求并恢复私有字节
→ 私有 Protobuf/MVT 转换为标准 MVT
→ MapLibre 使用合并后的 style layers 渲染调用 map.setStyle() 后,组件监听 style.load 并恢复自己的数据源和样式图层。dispose() 会解绑监听、移除图层和数据源、终止 Worker,并在最后一个 TDT 数据源销毁后移除自定义协议。协议或样式资源安装失败时,已创建的 Worker、数据源和样式图层会回滚,不残留半初始化资源。
只创建矢量数据源
需要自行管理 MapLibre 图层时,可以使用底层 API:
const bundle = createTdtVectorSource({
token: 'YOUR_TDT_TOKEN',
hosts: ['https://host-0.example.com'],
restoreMode: 'auto',
minZoom: 1,
maxZoom: 18,
decoderOptions: { requestTimeoutMs: 30_000 }
})
map.addSource('tdt-source', bundle.source)
// 先移除引用该 source 的所有 style layers 和 source
map.removeSource('tdt-source')
bundle.dispose()restoreMode 可选值为 auto、header、always、never,通常使用默认的 auto。
框架无关核心能力
本包从 tdt-vts-core 重新导出私有协议核心 API,包括:
createTdtVtsUrl、encodeTdtPk、decodeTdtPk、redactTdtTokenrestoreTdtBytes、decodeMvt、normalizePrivateMvt、encodeStandardMvtloadAndNormalizeTdtVtsTdtDecoderWorkerClient
常规业务优先使用 createTdtVtsLayer;只有自定义数据源、调试解码或适配其他渲染流程时才需要核心 API。
事件发射器
emitter 是包级轻量事件总线:
const handler = (payload: { id: string }) => console.log(payload.id)
emitter.on('selected', handler)
emitter.emit('selected', { id: 'feature-1' })
emitter.off('selected', handler)
emitter.off('selected') // 省略 handler 时清空该事件的全部监听
emitter.on('*', (type, payload) => {
console.log(type, payload)
})emitter 由 mitt 提供,与 map-ol-utils、venus-amap 的事件总线实现保持一致,因此 all 映射、'*' 通配事件和 off(type) 批量退订的语义与 mitt 完全相同。它与 MapLibre 的地图事件系统相互独立。
接口(API)总览
| 分类 | 导出函数 |
| --- | --- |
| 地图 | createMap、createMapInstance、getMapInstance、disposeMap、disposeInstance、resetMap、resetZoomAndCenter |
| 图层管理 | registerLayer、getLayerByName、getLayersDataList、changeLayer、changeLayersByNames、removeLayerByName、removeAllLayers |
| GeoJSON / 栅格 | createGeoJsonLayer、changeSource、createWmtsLayer、baseLayers、createMapWorldLayer、createBaseLayer、createZheJiangLayer |
| 业务矢量 | assembleGeoJson、createBusinessLayer、createVectorLineLayer、createVectorFaceLayer、appendFeaturesByLayer、createClusterLayer |
| 标记 | createText、createIconStyle、createMarker、createMarkersLayer、createElasticMarkerLayer、createMoveMarkerLayer、changeUnselectStyle |
| 海量点兼容 | buildMassMarksStyles、createMassMarksLayer、addMassMarksLayer、addFeaturesByLayer、updateMarksLayersByStatus |
| 交互 | createSelectByLayer、createLayerSelectByNames、activeFeatureByUid、resetActiveFeature、resetActiveVector、createDrawInteraction、getDrawStyle |
| 视图 | setFitViewByCoords、setFitViewByGeom、setFitViewByLayer、flyToAnimate |
| 覆盖物 | createOverlay、createAnimationMarker、removeOverlays |
| TDT VTS | createTdtVectorSource、createTdtVtsLayer,以及 tdt-vts-core 全部导出 |
主要类型导出包括 CustomMap、CreatedMap、CreateMapConfig、LayerHandle、LayerConfig、GeoJsonStyle、GeoJsonLayerConfig、DataItemConfig、ElasticDataItemConfig、MassData、VectorConfig、ClusterConfig、IconConfig、WmtsSourceOptions、GeometryType、GeometryCoordinates、MarkersLayerHandle、SelectInteraction、SelectFeature、OverlayOptions、OverlayHandle、CreateTdtSourceOptions 和 CreateTdtVtsLayerOptions。
与 map-ol-utils 的兼容边界
函数名称保持一致不代表对象类型相同:
| 能力 | maplibre-utils 返回值 / 行为 |
| ----------------------- | -------------------------------------------- |
| createMap | 带必备兼容方法的 CreatedMap |
| 图层创建函数 | LayerHandle,不是 OpenLayers Layer |
| createMarker | 原生 maplibregl.Marker |
| createOverlay | 带 setPosition() 的原生 maplibregl.Popup |
| activeFeatureByUid | 使用 MapLibre 要素标识与 feature-state |
| createDrawInteraction | 接收并返回调用方安装的 MapLibre IControl |
| createTdtVtsLayer | 与 OpenLayers 版都接受 Style URL 或对象 |
| 坐标 | [经度, 纬度],不接受 OpenLayers 投影对象 |
MapLibre 已原生支持的标记、弹窗、flyTo、fitBounds、GeoJSON 数据源、样式图层和事件,可以直接使用 MapLibre API;本包主要负责统一业务函数名称和生命周期。
仍有两个无法无依赖等价复制的引擎差异:OpenLayers 的 changeSource / changeLayer 执行多线程栅格像素运算,而 MapLibre 同名函数分别更新 GeoJSON 数据和切换可见性;OpenLayers 自带 Draw interaction,而 MapLibre 核心不提供绘制模块,因此 createDrawInteraction 接收调用方选择的 MapLibre Draw IControl。这两项如果要求完全同签名,需要额外确定栅格 shader 方案和具体 Draw 依赖。
开发与验证
pnpm --dir packages/maplibre-utils build
pnpm --dir packages/maplibre-utils check:types
pnpm --dir packages/maplibre-utils testtest 会依次执行构建、TypeScript 类型检查和公共函数名称兼容性测试。兼容性测试从 dist/index.mjs 导入发布入口及其声明,因此干净检出后必须先构建,再运行类型检查;直接执行 test 会自动完成这一顺序。
