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

maplibre-utils

v1.0.0

Published

MapLibre GL JS utilities with Tianditu private VTS support

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'
})

targetbaseLayer 外,其余配置透传给 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,无需填写 idsource,本包会生成并绑定。

更新数据

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,否则根据 styleformatversionmatrixSet 生成 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_ccva_cimg_ccia_cter_ccta_cibo_c
  • createBaseLayer 使用杭州城市大脑固定服务地址。
  • createZheJiangLayer 使用浙江政务服务固定 WMTS 地址,默认 Token 与 map-utils 一致。该服务使用 EPSG:4326 XYZ 矩阵,本包通过内部协议请求正确的源瓦片行,并重投影为 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 参数,以及 hasLinehasFacemarksStylesoffsetvectorConfig。其返回句柄会统一拥有选择交互及可选的线、面关联图层:创建中途失败会回滚,调用 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.activeunSelectHandler 会在切换选择、点击空白区域或销毁交互时调用。标记图层使用相同函数时会切换 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) 原样返回实现了 MapLibre IControl 的控件。
  • 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: 50duration: 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 和动画 Marker

createOverlay 返回带 setPosition() 兼容方法的原生 maplibregl.PopupcreateAnimationMarker 返回原生 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 工厂与请求超时配置 |

hostsstyle 都是必填项:

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 可选值为 autoheaderalwaysnever,通常使用默认的 auto

框架无关核心能力

本包从 tdt-vts-core 重新导出私有协议核心 API,包括:

  • createTdtVtsUrlencodeTdtPkdecodeTdtPkredactTdtToken
  • restoreTdtBytesdecodeMvtnormalizePrivateMvtencodeStandardMvt
  • loadAndNormalizeTdtVts
  • TdtDecoderWorkerClient

常规业务优先使用 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)
})

emittermitt 提供,与 map-ol-utilsvenus-amap 的事件总线实现保持一致,因此 all 映射、'*' 通配事件和 off(type) 批量退订的语义与 mitt 完全相同。它与 MapLibre 的地图事件系统相互独立。

接口(API)总览

| 分类 | 导出函数 | | --- | --- | | 地图 | createMapcreateMapInstancegetMapInstancedisposeMapdisposeInstanceresetMapresetZoomAndCenter | | 图层管理 | registerLayergetLayerByNamegetLayersDataListchangeLayerchangeLayersByNamesremoveLayerByNameremoveAllLayers | | GeoJSON / 栅格 | createGeoJsonLayerchangeSourcecreateWmtsLayerbaseLayerscreateMapWorldLayercreateBaseLayercreateZheJiangLayer | | 业务矢量 | assembleGeoJsoncreateBusinessLayercreateVectorLineLayercreateVectorFaceLayerappendFeaturesByLayercreateClusterLayer | | 标记 | createTextcreateIconStylecreateMarkercreateMarkersLayercreateElasticMarkerLayercreateMoveMarkerLayerchangeUnselectStyle | | 海量点兼容 | buildMassMarksStylescreateMassMarksLayeraddMassMarksLayeraddFeaturesByLayerupdateMarksLayersByStatus | | 交互 | createSelectByLayercreateLayerSelectByNamesactiveFeatureByUidresetActiveFeatureresetActiveVectorcreateDrawInteractiongetDrawStyle | | 视图 | setFitViewByCoordssetFitViewByGeomsetFitViewByLayerflyToAnimate | | 覆盖物 | createOverlaycreateAnimationMarkerremoveOverlays | | TDT VTS | createTdtVectorSourcecreateTdtVtsLayer,以及 tdt-vts-core 全部导出 |

主要类型导出包括 CustomMapCreatedMapCreateMapConfigLayerHandleLayerConfigGeoJsonStyleGeoJsonLayerConfigDataItemConfigElasticDataItemConfigMassDataVectorConfigClusterConfigIconConfigWmtsSourceOptionsGeometryTypeGeometryCoordinatesMarkersLayerHandleSelectInteractionSelectFeatureOverlayOptionsOverlayHandleCreateTdtSourceOptionsCreateTdtVtsLayerOptions

与 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 已原生支持的标记、弹窗、flyTofitBounds、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 test

test 会依次执行构建、TypeScript 类型检查和公共函数名称兼容性测试。兼容性测试从 dist/index.mjs 导入发布入口及其声明,因此干净检出后必须先构建,再运行类型检查;直接执行 test 会自动完成这一顺序。