@bonncloud/tianditu

天地图(Tianditu / 国家地理信息公共服务平台)全功能 TypeScript SDK —— JS API 4.0 组件控件 + Web 服务 HTTP API,同时支持 Node.js 与 Vue 3
535
Install
npm install @bonncloud/tianditu
Latest Version:0.2.0
Vue:3
License:MIT
Last Updated:Sep 24, 2026
Links: npm
Maintainer: funyunn

@bonncloud/tianditu

天地图(国家地理信息公共服务平台)全功能 TypeScript SDK —— JS API 4.0 地图组件/控件 + Web 服务 HTTP API,同一套类型定义同时服务 Node.js 与 Vue 3。

  • 浏览器端:loadTianditu() 脚本加载器、TiandituMap 面向对象封装、覆盖物/图层/控件/量算工厂、Vue 3 组件与组合式函数
  • Node.js 端:地名搜索 / 地理编码 / 逆地理编码 / 驾车 / 公交 / 行政区划 / 静态地图全量 HTTP 客户端
  • 坐标系工具:WGS84 / GCJ02 / BD09 互转(纯 TS,前后端通用)
  • 零运行时依赖,只依赖标准 fetch;ESM + CJS + .d.ts + sourcemap
  • 类型与封装严格按实测 API 边界定义,不存在的能力给出明确错误而不是静默失败

上图为 examples/vue3-demo 的真实运行截图(天地图矢量底图 + 控件 + 覆盖物 + 自定义控件)。


目录

  • 1. 包介绍
  • 2. 包功能
  • 3. 接口与组件调用示例
    • 3.1 脚本加载器 · 3.2 地图实例 · 3.3 覆盖物 · 3.4 图层 · 3.5 控件 · 3.6 量算/定位/绘制 · 3.7 几何工具
    • 3.8 Web 服务客户端 · 3.9 无状态函数式 API · 3.10 jsApi 便捷方法 · 3.11 坐标工具
    • 3.12 Vue 3 组件 · 3.13 Vue 3 组合式函数 · 3.14 类型导出 · 3.15 完整可运行示例
  • 4. 注意事项
  • 5. 版权

1. 包介绍

1.1 这是什么

天地图官方提供两条能力线,这个包把两条线都封装成了带完整类型的 TypeScript SDK:

能力线 运行位置 本包对应入口
JS API 4.0(在网页里嵌地图、画覆盖物、加控件) 浏览器 loadTianditu()、TiandituMap、覆盖物/图层/控件工厂、Vue 组件
Web 服务 HTTP API(搜索、地理编码、路径规划、静态图) 服务端(Node.js) TiandituClient、jsApi、无状态函数

1.2 为什么要多一层封装

天地图官方文档与实际运行时有几处已知不一致,直接照文档写代码会在运行时才炸。本包在实测基础上做了三件事:

  1. 只声明真实存在的 API。官方文档站有页面但主脚本里没有的类(T.Label、T.Geocoder、T.DrawTool 等)一律不封装,调用等价能力时给出明确错误或替代实现(详见 4.1)。
  2. 抹平已知坑。容器选择器归一化、setCenter 缺失、DivIcon 只接受 HTML 字符串、自定义控件不回调 onAdd、驾车规划返回 XML 且单位混用 —— 都在包内处理掉。
  3. Key 类型分离。浏览器端 Key 与服务端 Key 权限完全隔离,包内两套 API 各自明确要求对应的 Key(详见 4.2)。

1.3 包信息

项 值
包名 @bonncloud/tianditu
当前版本 0.1.2
许可 MIT
类型 type: module,ESM + CJS 双产物,附带 .d.ts 与 sourcemap
运行时依赖 无(只用标准 fetch 与 DOM/BOM API)
Node 版本 >=18(低版本可注入 fetch 实现)
Vue 支持 vue@^3.3,作为可选 peer 依赖,仅 vue 子路径需要
浏览器支持 支持 fetch 与 Promise 的现代浏览器

1.4 安装

npm i @bonncloud/tianditu
# 或
pnpm add @bonncloud/tianditu
yarn add @bonncloud/tianditu

Vue 3 项目无需额外安装 vue(已在项目中),SDK 不会强制引入它。

1.5 子路径导出

按需引入可以避免把整个 SDK 打进产物:

导入路径 内容
@bonncloud/tianditu 全部能力:加载器、地图封装、Web 服务客户端、坐标与量算工具、jsApi、类型
@bonncloud/tianditu/services 只要 Web 服务 HTTP API(Node.js 后端推荐)
@bonncloud/tianditu/browser jsApi 的具名导出形式(服务端 API 的 Promise 化便捷方法)
@bonncloud/tianditu/vue Vue 3 组件与组合式函数(会引入 vue)
@bonncloud/tianditu/types 纯类型定义(零运行时代码)
import { TiandituClient } from '@bonncloud/tianditu/services'   // Node.js 后端
import { jsApi } from '@bonncloud/tianditu/browser'             // 便捷方法
import { TdtMap, TdtMarker } from '@bonncloud/tianditu/vue'     // Vue 3
import type { SearchResponse } from '@bonncloud/tianditu/types' // 仅类型

2. 包功能

2.1 功能总览

模块 功能 入口
脚本加载 按需注入 JS API、单例去重、超时、扩展库与第三方依赖按序加载、SSR 环境明确报错 loadTianditu loadScripts loadPlugin getTianditu isTiandituLoaded setTiandituNamespace
地图实例 创建/包装地图、中心与层级、范围自适应、坐标与像素互转、覆盖物/控件/图层增删、事件、销毁 TiandituMap
覆盖物 图标、标注、文字标注、折线、多边形、圆、矩形、信息窗口 createIcon createMarker createTextMarker updateTextMarker createPolyline createPolygon createCircle createRectangle createInfoWindow
图层 官方 WMTS 底图(8 种图层 × 2 种投影)、自定义瓦片图层 createBaseLayer createTileLayer
控件 缩放、比例尺、底图切换、版权、自定义 DOM 控件 createZoomControl createScaleControl createMapTypeControl createCopyrightControl createCustomControl
底图类型 矢量 / 影像 / 影像混合 / 地形 / 地形混合 切换 getMapType setMapType createDefaultMapTypeItems
量算与定位 折线总长、逐段距离、距离格式化、交互式采点、浏览器定位 measureDistance measureSegments formatDistance drawShape locate moveTo
几何工具 坐标/像素/范围对象的归一化与构造 toLngLat toLngLatArray toPoint toLngLatBounds boundsFromString boundsOf lngLatToTuple resolveContainer resolvePoints
Web 服务 地名搜索 V2.0(8 种 queryType)、地理编码、逆地理编码、驾车规划、公交规划、行政区划 V2.0、静态地图 TiandituClient 及同名的无状态函数
便捷方法 服务端 API 的 Promise 化 + 结果归一化(坐标转数字、坐标串转数组、XML 解析) jsApi(geocode reverseGeocode reverseGeocodeDetail search searchPois drivingRoute transitRoute busLineSearch parseLngLatPath)
坐标工具 WGS84 / GCJ02 / BD09 三者互转、球面距离、坐标串解析与格式化 wgs84ToGcj02 gcj02ToWgs84 gcj02ToBd09 bd09ToGcj02 wgs84ToBd09 bd09ToWgs84 outOfChina distance parseLngLat formatLngLat round
Vue 3 10 个组件 + 4 个组合式函数 @bonncloud/tianditu/vue
错误处理 统一异常类型,携带 HTTP 状态码、天地图业务码、原始响应、请求地址;请求超时、失败重试、自定义 fetch TiandituServiceError
工程能力 完整 TS 类型、ESM/CJS 双格式、sourcemap、41 项单元测试、Vue 3 示例工程、真实 Key 联调脚本 pnpm typecheck pnpm test pnpm build scripts/live-check.mjs

2.2 能力明细

浏览器端(JS API 4.0)

  • 底图:normal satellite hybrid terrain terrainHybrid
  • 官方 WMTS 图层:vec(矢量底图)cva(矢量注记)eva(矢量英文注记)img(影像底图)cia(影像注记)ter(地形晕渲)cta(地形注记)ibo(境界),投影支持 c(经纬度 / EPSG:4326)与 w(球面墨卡托 / EPSG:900913)
  • 控件:Control.Zoom Control.Scale Control.MapType Control.Copyright + 任意 DOM 自定义控件(自动按 topleft / topright / bottomleft / bottomright 停靠)
  • 官方开源扩展库:热力图 HeatmapOverlay、车辆轨迹 CarTrack、D3 覆盖物 D3SvgOverlay、缓冲区 BufferTool、图片覆盖物 ImageOverlay

服务端(Web 服务 HTTP API)

服务 接口 SDK 方法
地名搜索 V2.0 GET /v2/search search()
地理编码 GET /geocoder(ds=) geocode()
逆地理编码 GET /geocoder(postStr=) reverseGeocode()
驾车规划 GET /drive drive()
公交规划 GET /transit transit()
行政区划 V2.0 GET /v2/administrative administrative()
静态地图 GET /staticimage staticMapUrl() / staticMap()

搜索服务的 queryType 覆盖全部 8 种取值:普通 1、视野内 2、周边 3、地名 7、多边形 10、行政区划 12、数据分类 13、统计 14。

2.3 明确不提供的能力

以下类在官方文档站有页面,但在 api?v=4.0 主脚本(138 KB)中检索出现 0 次,运行时为 undefined,因此本包不提供:

缺失的官方类 本包的替代方案
T.Label createTextMarker()(= T.Marker + T.DivIcon)
T.Geocoder / T.LocalSearch / T.DrivingRoute / T.WalkingRoute / T.TransitRoute / T.BusLineSearch 调 Web 服务 API:TiandituClient / jsApi(注意 Key 类型)
T.DrawTool / T.DistanceTool / T.AreaTool / T.MeasureTool drawShape() 采点 + measureDistance() / measureSegments()
T.Geolocation locate()(基于浏览器原生 navigator.geolocation)
T.Control.OverviewMap(鹰眼) createOverviewMapControl() 会直接抛错;可自行用第二个 T.Map 实例实现
T.MarkerClusterer(点聚合) 官方 opensource/openlibrary/MarkerClusterer.js 为 404,4.0 不可用;需自行实现或换用其他聚合方案

3. 接口与组件调用示例

3.1 脚本加载器

loadTianditu(options)

加载 JS API 并在全局注入 T 命名空间,返回带完整类型的 T。同一 URL 只会真正加载一次。

import { loadTianditu } from '@bonncloud/tianditu'

const T = await loadTianditu({
  tk: 'YOUR_BROWSER_TK',     // 【必填】浏览器端 Key
  version: '4.0',            // 可选,默认 '4.0'
  protocol: 'https',         // 可选,默认 'https'
  timeout: 15_000,           // 可选,默认 15000ms
  // apiUrl: 'https://your-cdn/tianditu/api?v=4.0&tk=xxx',  // 私有化部署时覆盖脚本地址
  // plugins: ['HeatmapOverlay'],                            // 需要一并加载的官方扩展库
  // dependencies: ['https://cdn.jsdelivr.net/npm/d3@7'],    // 扩展库依赖的第三方脚本(先加载)
})

const map = new T.Map('map')
参数 类型 说明
tk string 必填,浏览器端 Key
version string 默认 4.0
protocol 'http' | 'https' 默认 https
apiUrl string 自定义脚本地址,优先级最高
plugins (TiandituPluginName | string)[] 扩展库名称或 URL
dependencies string[] 第三方依赖脚本 URL,先于扩展库加载
timeout number 默认 15000
loadPlugin(name, options?) / loadScripts(urls, timeout?)
import { loadTianditu, loadPlugin, loadScripts } from '@bonncloud/tianditu'

// 主脚本加载完成后,单独加载官方扩展库
await loadTianditu({ tk: 'YOUR_BROWSER_TK' })
const T = await loadPlugin('HeatmapOverlay')

// 热力图使用前需先准备数据
const overlay = new T.HeatmapOverlay({ radius: 20 })
overlay.setDataSet({
  max: 100,
  data: [{ lng: 116.4, lat: 39.9, count: 60 }],
})
overlay.addDataPoint(116.42, 39.93, 30)
map.addOverLay(overlay)

// 需要第三方依赖的扩展库,例如 D3SvgOverlay 依赖 d3
await loadPlugin('D3SvgOverlay', {
  dependencies: ['https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'],
})

// 加载任意脚本(按序串行)
await loadScripts(['https://cdn.example.com/a.js', 'https://cdn.example.com/b.js'])

可用的扩展库名称(也可直接传 URL):

名称 用途
HeatmapOverlay 热力图
CarTrack 车辆轨迹动画
D3SvgOverlay D3 图形覆盖物
BufferTool 缓冲区分析
ImageOverlay 图片覆盖物
getTianditu() / isTiandituLoaded() / setTiandituNamespace(ns)
import { getTianditu, isTiandituLoaded, setTiandituNamespace } from '@bonncloud/tianditu'

if (!isTiandituLoaded()) {
  await loadTianditu({ tk: 'YOUR_BROWSER_TK' })
}
const T = getTianditu()   // 未加载时抛出明确错误

// SSR 预渲染、单元测试,或已用 <script> 手动引入时,手动注入命名空间
setTiandituNamespace(globalThis.T ?? null)

3.2 地图实例 TiandituMap

创建
import { TiandituMap } from '@bonncloud/tianditu'

// 方式一(推荐):自动加载脚本 + 创建地图
const map = await TiandituMap.create({
  tk: 'YOUR_BROWSER_TK',
  container: '#map',              // 支持 '#map' / 'map' / '#app .map' / HTMLElement
  center: [116.40769, 39.89945],
  zoom: 12,
  minZoom: 3,
  maxZoom: 18,
  scrollWheelZoom: true,          // 其余 MapOptions 原样透传
})

// 方式二:脚本已加载(如 <script> 引入)时同步创建
const map2 = TiandituMap.createSync('#map')

// 方式三:包装一个已存在的原生 T.Map 实例
const raw = new T.Map('map')
const map3 = TiandituMap.from(T, raw)

container 写法归一化:天地图原生 new T.Map() 只认不带 # 的元素 id,传 '#map' 会在内部抛 Cannot read properties of null (reading '_tdt_events')。resolveContainer() 已统一处理,找不到容器时给出明确错误。

状态与方法
// 中心点与层级
map.getCenter()                    // T.LngLat
map.getCenterTuple()               // [lng, lat]
map.setCenter([116.4, 39.9])       // 4.0 无 setCenter,内部用 panTo 实现
map.getZoom()                      // number
map.setZoom(14)
map.setView([116.4, 39.9], 14)     // 同时设中心点与层级

// 范围
const bounds = map.getBounds()                    // T.LngLatBounds
map.fitBounds([[116.3, 39.85], [116.5, 39.95]])   // 注意:坐标顺序是 [lng, lat]
map.setMaxBounds(bounds)
map.setMinZoom(4)
map.setMaxZoom(18)

// 距离与换算
map.getDistance([116.4, 39.9], [116.5, 39.9])        // 两点球面距离(米)
const p = map.lngLatToContainerPoint([116.4, 39.9])  // T.Point 容器像素坐标
const ll = map.containerPointToLngLat([100, 200])    // T.LngLat

// 底图与样式
map.setBaseMap('satellite')        // normal / satellite / hybrid / terrain / terrainHybrid
map.setStyle('indigo')             // 官方地图样式

// 容器尺寸变化后通知地图重算(侧边栏收起、弹窗打开等场景必调)
map.resize()

// 生命周期
map.getContainer()                 // HTMLElement
map.destroy()                      // 解绑全部事件、清空覆盖物与图层、清空容器

所有写操作(setCenter setZoom addOverlay addControl …)均返回 this,可链式调用:

map.setBaseMap('hybrid').setZoom(13).setCenter([116.4, 39.9])
事件
// on() 返回取消监听的函数
const off = map.on('click', (e) => {
  console.log(e.lnglat?.getLng(), e.lnglat?.getLat())
  console.log(e.containerPoint?.x, e.containerPoint?.y)
})
off()

// 只监听一次
map.once('load', () => console.log('地图加载完成'))

// 手动解绑
const handler = (e: unknown) => console.log(e)
map.on('moveend', handler)
map.off('moveend', handler)

支持的事件:click dblclick contextmenu mousemove mouseover mouseout movestart move moveend zoomstart zoomend addoverlay removeoverlay addcontrol removecontrol clearoverlays dragstart drag dragend layeradd layerremove load resize levels touchstart touchmove touchend longpress。

覆盖物 / 控件 / 图层 / 信息窗口
map.addOverlay(marker)             // 也可写 map.addOverLay(原生别名)
map.removeOverlay(marker)
map.getOverlays()                  // Overlay[]
map.clearOverlays()

map.addControl(control)
map.removeControl(control)

map.addLayer(layer)
map.removeLayer(layer)
map.getLayers()                    // TileLayer[]
map.clearLayers()

map.openInfoWindow(infoWindow, [116.4, 39.9])
map.closeInfoWindow()

原生 T.Map 实例始终保留在 map.raw 上,本类未覆盖的能力可直接调用:

map.raw.setMapType(TMAP_SATELLITE_MAP)

3.3 覆盖物工厂

所有工厂函数的第一个参数都是 T 命名空间。

import {
  createIcon, createMarker, createTextMarker, updateTextMarker,
  createPolyline, createPolygon, createCircle, createRectangle,
  createInfoWindow, resolvePoints,
} from '@bonncloud/tianditu'

const T = await loadTianditu({ tk: TK })
createIcon(T, options) —— 自定义图标
const icon = createIcon(T, {
  iconUrl: '/marker.png',
  iconSize: new T.Point(32, 40),
  iconAnchor: new T.Point(16, 40),   // 图标尖角相对左上角的偏移
})
createMarker(T, lnglat, options?) —— 点标注
const marker = createMarker(T, [116.4, 39.9], {
  icon,                    // 可选,T.Icon 或 T.DivIcon
  draggable: true,
  title: '北京',
  opacity: 1,
  zIndexOffset: 100,
  rotation: 0,
})
map.addOverlay(marker)

marker.addEventListener('click', (e) => console.log('点击标注', e))
marker.addEventListener('dragend', (e) => {
  const ll = marker.getLngLat()
  console.log('拖拽结束', ll.getLng(), ll.getLat())
})
createTextMarker(T, lnglat, options) —— 文字标注

天地图 4.0 没有 T.Label,官方做法是 Marker + DivIcon。本函数已封装,并对文本做了 HTML 转义。

const label = createTextMarker(T, [116.42, 39.93], {
  text: '文字标注',                    // 必填,内部已做 HTML 转义
  style: {
    color: '#d03050',
    fontSize: '14px',
    backgroundColor: 'rgba(255,255,255,0.9)',
    padding: '2px 6px',
    borderRadius: '4px',
  },
  className: 'my-label',              // 附加 class
  draggable: false,
  zIndexOffset: 10,
})
map.addOverlay(label)

// 更新文本(内部改写 .tdt-text-marker 的 textContent)
updateTextMarker(label, '新的文字')

style 的键名用 camelCase,内部会转成 kebab-case 后写入 style 属性(fontSize → font-size)。

createPolyline(T, points, options?) —— 折线
const line = createPolyline(T, [[116.3, 39.9], [116.5, 39.9]], {
  color: '#165dff',
  weight: 4,
  opacity: 0.9,
  lineStyle: 'solid',        // solid / dashed / dotted 或自定义字符串
  dashArray: '6,4',
  showLabel: true,           // 显示距离标签
  showArrow: false,
  clickable: true,
})
map.addOverlay(line)
createPolygon(T, points, options?) —— 多边形
const polygon = createPolygon(
  T,
  [[116.3, 39.85], [116.45, 39.85], [116.45, 39.95]],
  { color: '#00b42a', weight: 2, fillColor: '#00b42a', fillOpacity: 0.2 },
)
map.addOverlay(polygon)
createCircle(T, center, radius, options?) —— 圆
const circle = createCircle(T, [116.35, 39.93], 1500, {   // radius 单位:米
  color: '#ff7d00', weight: 2, fillColor: '#ff7d00', fillOpacity: 0.15,
})
map.addOverlay(circle)
createRectangle(T, sw, ne, options?) —— 矩形
const rect = createRectangle(T, [116.3, 39.85], [116.45, 39.95], {
  color: '#722ed1', weight: 2, fillColor: '#722ed1', fillOpacity: 0.15,
})
map.addOverlay(rect)
createInfoWindow(T, content?, options?) —— 信息窗口
const iw = createInfoWindow(T, '<strong>标题</strong><br/>内容支持 HTML', {
  minWidth: 120,
  maxWidth: 320,
  offset: new T.Point(0, -30),
  autoPan: true,
  closeButton: true,
  closeOnClick: true,
})

map.openInfoWindow(iw, [116.4, 39.9])   // 由地图打开(推荐)
map.closeInfoWindow()

// 也可以自行控制生命周期
iw.setLngLat(toLngLat(T, [116.4, 39.9]))
iw.open()
iw.close()
iw.isOpen()                             // boolean
iw.setContent('<b>新内容</b>')
resolvePoints(T, points) —— 批量拿到 T.LngLat[]
const lnglats = resolvePoints(T, [[116.4, 39.9], { lng: 116.5, lat: 39.9 }])

3.4 图层工厂

createBaseLayer(T, tk, layer?, options?) —— 官方 WMTS 底图
import { createBaseLayer, type BaseLayerName } from '@bonncloud/tianditu'

// 叠加影像注记
map.addLayer(createBaseLayer(T, TK, 'cia', { projection: 'w' }))

// 经纬度投影的矢量底图 + 注记
map.addLayer(createBaseLayer(T, TK, 'vec', { projection: 'c' }))
map.addLayer(createBaseLayer(T, TK, 'cva', { projection: 'c' }))

// 自定义瓦片域名序号与格式
map.addLayer(createBaseLayer(T, TK, 'img', { projection: 'w', subdomain: 3, format: 'tiles' }))

// 完全自定义 URL 模板(优先级最高)
map.addLayer(
  createBaseLayer(T, TK, 'vec', {
    urlTemplate: 'https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&...&tk=' + TK,
  }),
)
layer 含义 projection 含义
vec 矢量底图 w 球面墨卡托 EPSG:900913(默认)
cva 矢量注记 c 经纬度 EPSG:4326
eva 矢量英文注记
img 影像底图
cia 影像注记
ter 地形晕渲
cta 地形注记
ibo 境界

options 额外支持 subdomain(默认 0)、format(默认 tiles)、urlTemplate,以及全部 TileLayerOptions。

createTileLayer(T, url, options?) —— 自定义瓦片图层
import { createTileLayer } from '@bonncloud/tianditu'

const layer = createTileLayer(T, 'https://tile.example.com/{z}/{x}/{y}.png', {
  minZoom: 3,
  maxZoom: 18,
  opacity: 0.8,
  zIndex: 10,
  tileSize: 256,
  subdomains: '0123',
  detectRetina: true,
  attribution: '© 我的瓦片服务',
  crossOrigin: true,
})
map.addLayer(layer)
map.removeLayer(layer)
map.clearLayers()

3.5 控件工厂

import {
  createZoomControl, createScaleControl, createMapTypeControl,
  createCopyrightControl, createCustomControl, createOverviewMapControl,
  createDefaultMapTypeItems, getMapType, setMapType,
} from '@bonncloud/tianditu'
内置控件
map.addControl(createZoomControl(T, { position: 'topleft' }))
map.addControl(createScaleControl(T, { position: 'bottomleft' }))

// 底图切换控件:默认 5 项(地图 / 卫星 / 卫星混合 / 地形 / 地形混合)
map.addControl(createMapTypeControl(T, undefined, { position: 'topright' }))

// 自定义底图切换项
map.addControl(
  createMapTypeControl(
    T,
    [
      { title: '地图', icon: 'https://.../vector.png', layer: getMapType('normal') },
      { title: '卫星', icon: 'https://.../satellite.png', layer: getMapType('satellite') },
    ],
    { position: 'topright' },
  ),
)

// 版权控件
map.addControl(createCopyrightControl(T, { position: 'bottomright', content: '© 天地图' }))

position 取值:'topleft' 'topright' 'bottomleft' 'bottomright';offset 支持 [x, y] 或 { x, y }。

createCustomControl(element, options?) —— 任意 DOM 控件

天地图对非 Control 子类的对象不会回调 onAdd,导致控件停在页面左上角。本函数在创建时就应用定位,并把地图容器设为定位上下文。

const el = document.createElement('div')
el.className = 'my-control'
el.innerHTML = '<button>定位</button>'
el.querySelector('button')!.addEventListener('click', () => locate())

const control = createCustomControl(el, { position: 'bottomright', offset: [0, -10] })
map.addControl(control)

// 显示控制
control.hide()
control.show()
control.isVisible()
map.removeControl(control)
底图类型切换
map.setBaseMap('satellite')            // 封装层提供
setMapType(map.raw, 'terrainHybrid')   // 底层函数式调用
const type = getMapType('normal')      // 拿到官方 TMAP_NORMAL_MAP 常量
createDefaultMapTypeItems()            // 生成默认的底图切换项数组
名称 对应全局常量
normal TMAP_NORMAL_MAP
satellite TMAP_SATELLITE_MAP
hybrid TMAP_HYBRID_MAP
terrain TMAP_TERRAIN_MAP
terrainHybrid TMAP_TERRAIN_HYBRID_MAP
createOverviewMapControl() —— 鹰眼
// 天地图 JS API 4.0 不存在 T.Control.OverviewMap,调用会直接抛出明确错误
createOverviewMapControl(T)   // ✗ Error: 天地图 JS API 4.0 不存在 T.Control.OverviewMap

如需缩略图,请用第二个 T.Map 实例自行实现。


3.6 量算、定位与绘制

天地图 4.0 没有 DrawTool / DistanceTool / AreaTool / MeasureTool / Geolocation,本包基于确实存在的 API 提供等价能力。

import {
  measureDistance, measureSegments, formatDistance,
  drawShape, locate, moveTo,
} from '@bonncloud/tianditu'
测距
const points: [number, number][] = [[116.35, 39.92], [116.45, 39.90], [116.55, 39.95]]

measureDistance(T, map.raw, points)     // 折线总长(米),逐段累加官方 getDistance
measureSegments(T, map.raw, points)     // [段1, 段2](米),长度比 points 少 1
formatDistance(1234.567)                // '1.23 km'
formatDistance(850)                     // '850 m'
formatDistance(Number.NaN)              // '-'
drawShape(T, map, options?) —— 交互式采点

替代官方 DrawTool:单击加点、双击结束,返回 Promise<T.LngLat[]>。

const lnglats = await drawShape(T, map.raw, {
  onAdd: (lnglat, all) => console.log(`已采集 ${all.length} 个点`, lnglat.getLng()),
})

// 采完接量算
if (lnglats.length > 1) {
  console.log(formatDistance(measureDistance(T, map.raw, lnglats)))
}

// 也可以用来画多边形
if (lnglats.length > 2) {
  map.addOverlay(createPolygon(T, lnglats, { fillColor: '#00b42a' }))
}
locate(options?) —— 浏览器定位

基于原生 navigator.geolocation,返回 WGS84 坐标(天地图底图为 CGCS2000,差异厘米级可直接使用)。

const { lng, lat, accuracy, lnglat } = await locate({
  enableHighAccuracy: true,
  timeout: 10_000,
  maximumAge: 30_000,
})

map.setCenter([lng, lat])
map.addOverlay(createMarker(T, [lng, lat], { title: `精度 ${Math.round(accuracy)} 米` }))
moveTo(map, lnglat, zoom?)
moveTo(map.raw, new T.LngLat(116.4, 39.9))        // 只移动中心
moveTo(map.raw, new T.LngLat(116.4, 39.9), 15)    // 同时改层级

3.7 几何工具

把各种「像坐标」的输入统一成天地图原生对象,省掉手写 new T.LngLat()。

import {
  toLngLat, toLngLatArray, toPoint, toLngLatBounds,
  boundsFromString, boundsOf, lngLatToTuple, resolveContainer,
} from '@bonncloud/tianditu'

// 输入支持:[lng, lat] 数组、{ lng, lat }、{ lon, lat }、已有 T.LngLat
toLngLat(T, [116.4, 39.9])
toLngLat(T, { lng: 116.4, lat: 39.9 })
toLngLat(T, { lon: 116.4, lat: 39.9 })
toLngLat(T, existingLngLat)

toLngLatArray(T, [[116.4, 39.9], [116.5, 39.9]])

// 像素坐标:[x, y] 或 { x, y }
toPoint(T, [100, 200])
toPoint(T, { x: 100, y: 200 })

// 范围
toLngLatBounds(T, [116.3, 39.85], [116.5, 39.95])   // 西南角 → 东北角
boundsFromString(T, '116.3,39.85,116.5,39.95')      // "minx,miny,maxx,maxy"
boundsOf(T, [[116.3, 39.85], [116.5, 39.95]])       // 由点集自动计算外包矩形

lngLatToTuple(new T.LngLat(116.4, 39.9))            // [116.4, 39.9]

// 容器归一化
const el = resolveContainer('#map')   // 支持 '#map' / 'map' / '#app .map' / HTMLElement

boundsOf 在点集为空时会抛出 TypeError。


3.8 Web 服务客户端 TiandituClient

⚠️ 必须使用「服务端」类型的 Key,且必须在服务端调用(浏览器直连会被拒,见 4.3)。

import { TiandituClient } from '@bonncloud/tianditu/services'

const client = new TiandituClient({
  tk: process.env.TIANDITU_SERVER_TK!,
  baseURL: 'https://api.tianditu.gov.cn',   // 可选,默认值即此
  timeout: 8000,                            // 可选,默认 10000ms
  retry: 1,                                 // 可选,默认 0
  headers: { 'User-Agent': 'my-app/1.0' },  // 可选
  // fetch: myFetch,                        // 可选,Node < 18 时注入实现
})
配置项 类型 默认 说明
tk string — 必填,服务端 Key
baseURL string https://api.tianditu.gov.cn 服务基地址,可指向自有转发端点
timeout number 10000 单次请求超时(毫秒)
retry number 0 失败重试次数,重试间隔按次数递增
fetch typeof fetch 全局 fetch 自定义实现
headers Record<string, string> — 额外请求头

所有方法都接受可选的第二个参数 extra:{ signal?: AbortSignal; timeout?: number }。

client.search(params, extra?) —— 地名搜索 V2.0
import { SearchQueryType } from '@bonncloud/tianditu/services'

// 周边搜索(queryType = 3)
const nearby = await client.search({
  queryType: SearchQueryType.Nearby,
  keyWord: '公园',
  pointLonlat: '116.48016,39.93136',
  queryRadius: 3000,       // 米,10 公里内
  start: 0,
  count: 20,
  show: 2,                 // 1 基本信息 / 2 详细信息
})
console.log(nearby.pois?.[0]?.name, nearby.pois?.[0]?.lonlat, nearby.pois?.[0]?.distance)

// 普通搜索(queryType = 1)需要 mapBound + level
const normal = await client.search({
  queryType: SearchQueryType.Normal,
  keyWord: '北京大学',
  mapBound: '116.02,39.83,116.65,39.99',
  level: 12,
  start: 0,
  count: 10,
})

// 视野内搜索(queryType = 2)
await client.search({ queryType: SearchQueryType.InView, keyWord: '医院', mapBound: '-180,-90,180,90', level: 12 })

// 地名搜索(queryType = 7)
await client.search({
  queryType: SearchQueryType.PlaceName, keyWord: '西湖',
  mapBound: '-180,-90,180,90', level: 12, specify: '156330000',
})

// 多边形搜索(queryType = 10),首尾坐标相同
await client.search({
  queryType: SearchQueryType.Polygon, keyWord: '学校',
  polygon: '116.3,39.9,116.5,39.9,116.5,39.95,116.3,39.95,116.3,39.9',
})

// 行政区划区域搜索(queryType = 12)
await client.search({ queryType: SearchQueryType.Region, keyWord: '银行', specify: '156110000' })

// 数据分类搜索(queryType = 13)
await client.search({ queryType: SearchQueryType.DataType, keyWord: '餐饮', specify: '156110000' })

// 统计搜索(queryType = 14)
const stat = await client.search({ queryType: SearchQueryType.Statistics, keyWord: '加油站', specify: '156110000' })
console.log(stat.statistics?.[0]?.count)

// 超时 / 取消
const ac = new AbortController()
await client.search({ queryType: SearchQueryType.Normal, keyWord: '公园', mapBound: '-180,-90,180,90', level: 12 }, { signal: ac.signal, timeout: 5000 })
ac.abort()

响应字段:resultType(1 POI / 2 统计 / 3 行政区 / 4 建议词 / 5 线路)、count、pois、statistics、area、lineData、prompt、status。

client.geocode(params, extra?) / client.reverseGeocode(params, extra?)
// 地址 → 坐标
const geo = await client.geocode({ keyWord: '北京市海淀区莲花池西路28号' })
console.log(geo.location?.lon, geo.location?.lat, geo.location?.level, geo.location?.score)

// 坐标 → 地址
const rev = await client.reverseGeocode({ lon: 116.37304, lat: 39.92594, ver: 1 })
console.log(rev.result?.formatted_address)
console.log(rev.result?.addressComponent.poi, rev.result?.addressComponent.road)
client.drive(params, extra?) —— 驾车规划

官方该接口返回的是 XML(不是 JSON),SDK 已解析成 DriveResponse,原始文本保留在 raw。

const drive = await client.drive({
  orig: '116.35,39.92',
  dest: '116.39,39.90',
  mid: '116.36,39.91;116.38,39.91',   // 可选,途经点用分号分隔
  style: 0,                            // 0 最快 / 1 最短 / 2 避开高速 / 3 步行
})

console.log(drive.format)       // 'xml'
console.log(drive.distance)     // '7.0' —— ⚠️ 单位是「公里」
console.log(drive.duration)     // '482.0' —— 单位是「秒」

drive.route?.steps?.forEach((s, i) => {
  console.log(i, s.instruction, s.distanceText, s.distance)  // 例:'1.2公里' 1200(⚠️ 单位是「米」)
})

单位不一致是官方响应自带的坑:顶层 <distance> 是公里,而按段的里程是米。DriveStep.duration 官方文档中的 streetDistance 字段在 <routes>/<item> 里其实不存在,SDK 会从指引文本 strguide 解析(解析不到时为 NaN / 空串)。

client.transit(params, extra?) —— 公交规划
import { BusLineType } from '@bonncloud/tianditu/services'

const bus = await client.transit({
  startPosition: '116.42,39.93',
  endPosition: '116.34,39.93',
  lineType: BusLineType.Fast,        // 按位可组合:Fast 1 / LessTransfer 2 / LessWalk 4 / NoSubway 8
})

console.log(bus.resultCode)          // 0 正常;1 找不到起点;2 找不到终点;3 规划失败;4/5 建议步行;6 参数错误
console.log(bus.hasSubway)           // 0 不含地铁 / 1 含地铁
bus.results[0]?.lines?.forEach((line) => {
  console.log(line.lineName)         // 如 '3路—4路—5路'
  line.segments.forEach((seg) => console.log(seg.segmentType, seg.segmentLine.segmentDistance))
})

resultCode 为 3(规划失败)或 6(参数错误)时抛错;1 2 4 5 属于「有提示但可用」的返回,不抛错。

client.administrative(params, extra?) —— 行政区划 V2.0

官方文档:https://lbs.tianditu.gov.cn/server/administrative2.html

import { AdminChildLevel } from '@bonncloud/tianditu/services'

const admin = await client.administrative({
  keyword: '156110000',                // 行政区划名称或 9 位行政区划编码
  childLevel: AdminChildLevel.One,     // '0' 不下级 / '1' 下一级 / '2' 下两级 / '3' 下三级
  extensions: true,                    // 需要轮廓数据
})

console.log(admin.status)              // 200 正常,其他请看 admin.message
admin.data.district.forEach((item) => {
  console.log(item.name, item.gb, item.pgb, item.level)
  console.log(item.center.lng, item.center.lat)
  console.log(item.boundary)           // WKT MULTIPOLYGON 串(extensions: true 时才有)
  console.log(item.children?.length)   // 下级行政区(childLevel 非 0 时才有)
})
  • status === 200 只代表请求成功,不代表一定查到数据:查无结果时 data.district 为空数组, SDK 不抛错,请自行判断 data.district.length(官方响应里没有 V1.0 的 returncode)。
  • 参数:keyword 必填,支持行政区划名称或编码,由官方自动识别,名称支持模糊查询; 只传一个字符时官方只返回 suggestion,不返回 district。childLevel 默认 '0'、extensions 默认 false。
  • 条目字段:gb(行政区划编码)、pgb(上级行政区划编码,官方参数表未列出但实测稳定存在)、 center: { lng, lat }、level: number(2 区县 / 3 市 / 4 省 / 5 国家)、boundary(WKT 串, extensions: false 时该字段不存在而非空串)。
  • 持续拿到空结果要先排查限流:实测官方在触发限流时可能不返回错误码,而是返回 status: 200 且 district 为空数组(显式形式是 {"code":302010,"msg":"该tk已限流"})。 所以不要把空数组当成「该名称不存在」——先确认配额与调用频率。 名称查询(keyword: '北京')与编码查询(keyword: '156110000')都可以正常返回数据。
  • 官方 V1.0 已于 2024-06-30 下线:旧的 GET /administrative(postStr 传参, 含 searchWord / searchType / needSubInfo / needAll / needPolygon / needPre) 现已恒返回 HTTP 418,0.2.0 起已彻底移除,相关能力映射见 CHANGELOG.md。
client.staticMapUrl(params?) / client.staticMap(params?, extra?)
// 生成可直接用于 <img src> 的地址
const url = client.staticMapUrl({
  center: '116.40,39.93',
  zoom: 12,
  width: 800,            // 最大 1024
  height: 600,           // 最大 1024
  layers: 'vec_c,cva_c', // vec_c / cva_c / img_c / cia_c / ter_c / cta_c / ibo_c(_c 经纬度 / _w 墨卡托)
  markers: '116.40,39.93,北京',
  labels: '116.40,39.93',
  bbox: '116.3,39.85,116.5,39.95',
})

// 拉取图片二进制
const buffer = await client.staticMap({ center: '116.40,39.93', zoom: 10, width: 600, height: 400 })
fs.writeFileSync('map.png', Buffer.from(buffer))
client.raw(path, params?, extra?) —— 逃生舱

调用 SDK 未封装的任意天地图 Web 服务接口:

const res = await client.raw('/staticimage', { center: '116.4,39.93', width: 400, height: 300 })
client.tk / client.baseURL
client.tk          // readonly string
client.baseURL     // readonly string(已去掉结尾斜杠)

3.9 无状态函数式 API

不持有客户端实例,适合一次性调用或配合 tree-shaking。第二个参数传 Key 字符串或 TiandituClientOptions。

import { search, geocode, reverseGeocode, drive, transit, administrative, staticMapUrl } from '@bonncloud/tianditu/services'

const tk = process.env.TIANDITU_SERVER_TK!

const res = await search({ queryType: 1, keyWord: '公园', mapBound: '-180,-90,180,90', level: 12 }, tk)
const geo = await geocode({ keyWord: '北京市海淀区莲花池西路28号' }, { tk, timeout: 8000 })
const rev = await reverseGeocode({ lon: 116.37304, lat: 39.92594 }, tk)
const dri = await drive({ orig: '116.35,39.92', dest: '116.39,39.90' }, tk)
const bus = await transit({ startPosition: '116.42,39.93', endPosition: '116.34,39.93', lineType: 1 }, tk)
const adm = await administrative({ keyword: '156110000', childLevel: '1' }, tk)
const img = staticMapUrl(tk, { center: '116.40,39.93', zoom: 10, width: 400, height: 300 })
其他工具函数
import { DEFAULT_BASE_URL, parseDriveResponse, parseDistanceFromGuide, createClient } from '@bonncloud/tianditu/services'
import { TiandituServiceError, getJSON, toPostStr } from '@bonncloud/tianditu/services'

DEFAULT_BASE_URL                                   // 'https://api.tianditu.gov.cn'
parseDistanceFromGuide('沿朝阳北路走300米并向北直行')   // { meters: 300, text: '300米' }
parseDistanceFromGuide('沿崇文门东大街走1.2公里并..')   // { meters: 1200, text: '1.2公里' }
parseDistanceFromGuide('沿朝阳北路行驶到目的地。')      // null
parseDriveResponse(xmlText)                        // DriveResponse
createClient('YOUR_TK')                            // TiandituClient
toPostStr({ keyWord: '公园', start: 0 })            // '{"keyWord":"公园","start":0}'
错误处理
import { TiandituServiceError } from '@bonncloud/tianditu'

try {
  await client.search({ queryType: 1, keyWord: '公园', mapBound: '-180,-90,180,90', level: 12 })
} catch (err) {
  if (err instanceof TiandituServiceError) {
    err.message   // '搜索失败:xxx'
    err.status    // HTTP 状态码,网络层失败为 0
    err.code      // 天地图业务码,如 infocode / status / resultCode
    err.payload   // 原始响应文本
    err.url       // 请求地址
  }
}

3.10 jsApi —— 服务端 API 的便捷方法

在 TiandituClient 之上做了结果归一化(坐标转数字、坐标串转数组、XML 解析),一行拿到能直接用的结果。底层同样是 Web 服务 HTTP API,需要服务端 Key。

import { jsApi, TiandituClient, SearchQueryType } from '@bonncloud/tianditu'
// 或具名引入:import { geocode, searchPois } from '@bonncloud/tianditu/browser'

鉴权参数 auth 支持两种写法:

const auth = process.env.TIANDITU_SERVER_TK!                       // ① 传 Key 字符串(临时创建客户端)
const auth2 = new TiandituClient({ tk, timeout: 8000, retry: 1 })  // ② 传客户端实例(推荐,复用配置)
jsApi.geocode(auth, address)
const loc = await jsApi.geocode(auth, '北京市海淀区莲花池西路28号')
// → { lng: 116.290158, lat: 39.894696, level: '门址', score: 100, raw }
console.log(loc?.lng, loc?.lat, loc?.level, loc?.score)
// 未匹配到时返回 null
jsApi.reverseGeocode(auth, lnglat) / jsApi.reverseGeocodeDetail(auth, lnglat)
// 只取格式化地址文本
const addr = await jsApi.reverseGeocode(auth, [116.37304, 39.92594])
// → '北京市西城区西什库大街31号院23'
const addr2 = await jsApi.reverseGeocode(auth, { lon: 116.37304, lat: 39.92594 })

// 完整结构化结果(省市区、道路、周边 POI 等)
const detail = await jsApi.reverseGeocodeDetail(auth, [116.37304, 39.92594])
console.log(detail?.addressComponent.poi, detail?.addressComponent.road)
jsApi.searchPois(auth, params) / jsApi.search(auth, params)
// 归一化 POI 列表
const pois = await jsApi.searchPois(auth, {
  queryType: SearchQueryType.Nearby,
  keyWord: '公园',
  pointLonlat: '116.48016,39.93136',
  queryRadius: 3000,
})
// → [{ name, address, lng, lat, phone, province, city, county, typeName, distance, raw }]
pois.forEach((p) => console.log(p.name, p.lng, p.lat, p.distance))

// 需要 statistics / area / lineData 时取原始响应
const raw = await jsApi.search(auth, { queryType: SearchQueryType.Normal, keyWord: '北京大学', mapBound: '-180,-90,180,90', level: 12 })
console.log(raw.count, raw.pois?.length)
jsApi.drivingRoute(auth, params)
const route = await jsApi.drivingRoute(auth, { orig: '116.35,39.92', dest: '116.39,39.90' })
// → { distance: 5.82, duration: 720, path: [[lng, lat], ...], raw }
//    ↑ 全程距离单位是「公里」

console.log(route.distance, route.duration, route.path.length)
map.addOverlay(createPolyline(T, route.path, { color: '#165dff' }))

// 按段距离单位是「米」(从指引文本解析)
route.raw.route?.steps?.forEach((s) => console.log(s.distanceText, s.distance))
// → '400米'   400
// → '1.2公里' 1200
jsApi.transitRoute(auth, params) / jsApi.busLineSearch(auth, keyword, options?)
const bus = await jsApi.transitRoute(auth, {
  startPosition: '116.42,39.93',
  endPosition: '116.34,39.93',
  lineType: 1,
})

// 天地图没有独立的「公交线路查询」接口,这里用搜索服务检索线路名并取 lineData
const lines = await jsApi.busLineSearch(auth, '1路', {
  specify: '156110000',              // 限定行政区国标码(9 位)
  mapBound: '116.02,39.83,116.65,39.99',
  level: 12,
  start: 0,
  count: 10,
})
// → [{ stationNum, poiType, name, uuid }]
jsApi.parseLngLatPath(coordinateString)
jsApi.parseLngLatPath('116.4,39.9;116.5,39.9')   // [[116.4, 39.9], [116.5, 39.9]]
jsApi.parseLngLatPath('116.4,39.9 116.5,39.9')   // 分号、空格、换行均可
jsApi.parseLngLatPath('')                        // []

jsApi 全部方法:geocode reverseGeocode reverseGeocodeDetail search searchPois drivingRoute transitRoute busLineSearch parseLngLatPath。


3.11 坐标工具

纯 TypeScript 实现,Node.js 与浏览器通用,无任何地图依赖。

import {
  wgs84ToGcj02, gcj02ToWgs84, gcj02ToBd09, bd09ToGcj02,
  wgs84ToBd09, bd09ToWgs84, outOfChina,
  distance, parseLngLat, formatLngLat, round,
} from '@bonncloud/tianditu'

// WGS84(GPS)↔ GCJ02(高德 / 腾讯)
wgs84ToGcj02(116.404, 39.915)          // [116.4102445, 39.9164043]
gcj02ToWgs84(116.4102445, 39.9164043)  // [116.404, 39.915],迭代逼近,往返误差约 1e-14

// GCJ02 ↔ BD09(百度)
gcj02ToBd09(116.404, 39.915)
bd09ToGcj02(116.404, 39.915)

// 一步到位
wgs84ToBd09(116.404, 39.915)
bd09ToWgs84(116.404, 39.915)

// 境外坐标不做偏移
outOfChina(116.404, 39.915)       // false

// 球面距离(米,保留 2 位小数)
distance([116.4, 39.9], [121.47, 31.23])   // 1068272.85

// 坐标串解析与格式化
parseLngLat('116.404, 39.915')    // [116.404, 39.915],解析失败抛 TypeError
formatLngLat([116.404, 39.915])   // '116.404,39.915'
formatLngLat(116.404, 39.915)     // '116.404,39.915'

// 保留小数位
round(116.404123456, 6)           // 116.404123

gcj02ToWgs84 因官方偏移算法不可逆,采用 3 次迭代逼近(单次反向误差约 1~2 米,3 次后为厘米级)。


3.12 Vue 3 组件

import {
  TdtMap, TdtMarker, TdtPolyline, TdtPolygon, TdtCircle, TdtRectangle,
  TdtInfoWindow, TdtLabel, TdtTileLayer, TdtControl,
} from '@bonncloud/tianditu/vue'

除 TdtMap 外,所有覆盖物/图层/控件组件都必须放在 <TdtMap> 内部(通过注入获取地图实例)。

TdtMap
Prop 类型 默认 说明
tk string — 必填,浏览器端 Key
center LngLatLike — 中心点 [lng, lat]
zoom number 12 缩放级别
minZoom / maxZoom number — 缩放范围
projection string EPSG:900913 地图投影
plugins / dependencies string[] — 一并加载的扩展库 / 其依赖
mapOptions MapOptions — 原生 T.Map 的其它配置项
controls TdtBuiltinControls — 内置控件开关,见下
width / height string 100% / 100% 容器尺寸
事件 载荷 说明
@ready TiandituMap 地图初始化完成(此时控件已挂载)
@error Error 脚本加载或初始化失败

默认插槽在 ready 后才渲染,并透出 map 与 T:

<TdtMap :tk="TK" :center="[116.4, 39.9]" :zoom="12" @ready="onReady">
  <template #default="{ map, T }">
    <span v-if="map">当前层级 {{ map.getZoom() }}</span>
  </template>
</TdtMap>

controls 内置开关(传 true 用默认位置,传对象自定义):

<TdtMap
  :tk="TK"
  :controls="{
    zoom: true,
    scale: { position: 'bottomleft' },
    mapType: { position: 'topright' },
    copyright: { position: 'bottomright', content: '© 天地图' },
  }"
/>

通过 ref 可拿到组件暴露的实例:

<script setup lang="ts">
import { ref } from 'vue'
import { TdtMap } from '@bonncloud/tianditu/vue'

const mapRef = ref<InstanceType<typeof TdtMap> | null>(null)
// mapRef.value?.map / .T / .ready / .init / .container
</script>
TdtMarker
Prop 类型 默认 说明
position LngLatLike — 必填,[lng, lat]
iconUrl string — 自定义图标地址
iconSize [number, number] [25, 41] 图标尺寸
iconAnchor [number, number] — 图标锚点
draggable boolean false 是否可拖拽
title string — 悬浮提示
zIndexOffset number — 层级偏移
opacity number 1 透明度
options MarkerOptions — 原生配置透传

事件:@click @dblclick @dragend @mouseover @mouseout(载荷为事件对象)。暴露 marker。

<TdtMarker :position="[116.4, 39.9]" title="北京" draggable @dragend="onDragEnd" />
<TdtMarker :position="p" icon-url="/pin.png" :icon-size="[32, 40]" :icon-anchor="[16, 40]" @click="onClick" />
TdtPolyline
Prop 类型 默认
points LngLatLike[] — (必填)
color string #3388ff
weight number 3
opacity number 0.9
lineStyle LineStyle —
showLabel boolean —
options PolylineOptions —

事件:@click。暴露 polyline。

<TdtPolyline :points="[[116.3, 39.9], [116.5, 39.9]]" color="#165dff" :weight="5" show-label />
TdtPolygon
Prop 类型 默认
points LngLatLike[] — (必填)
color string #3388ff
weight number 2
opacity number 0.9
fillColor string #3388ff
fillOpacity number 0.2
lineStyle LineStyle —
options PolygonOptions —

事件:@click。暴露 polygon。

<TdtPolygon :points="area" fill-color="#00b42a" :fill-opacity="0.25" />
TdtCircle
Prop 类型 默认
center LngLatLike — (必填)
radius number — (必填,单位米)
color / weight / opacity string / number / number #3388ff / 2 / 0.9
fillColor / fillOpacity string / number #3388ff / 0.2
options CircleOptions —

事件:@click。暴露 circle。

<TdtCircle :center="[116.35, 39.93]" :radius="1500" fill-color="#ff7d00" />
TdtRectangle
Prop 类型 默认
sw LngLatLike — (必填,西南角)
ne LngLatLike — (必填,东北角)
color / weight string / number #3388ff / 2
fillColor / fillOpacity string / number #3388ff / 0.2
options RectangleOptions —

事件:@click。暴露 rectangle。

<TdtRectangle :sw="[116.3, 39.85]" :ne="[116.45, 39.95]" fill-color="#722ed1" />
TdtInfoWindow
Prop 类型 默认 说明
content string — 内容,支持 HTML 片段
position LngLatLike — 必填,显示位置
visible boolean true 是否显示,切 false 会关闭
offset [number, number] — 偏移像素
options InfoWindowOptions — 原生配置透传

暴露 infoWindow。

<TdtInfoWindow v-if="show" :position="pos" :content="`<strong>${title}</strong><br/>${address}`" :offset="[0, -30]" />
TdtLabel

天地图 4.0 无 T.Label,本组件基于 Marker + DivIcon 实现。

Prop 类型 默认
text string — (必填)
position LngLatLike — (必填)
color string #1f2329
fontSize number 12
backgroundColor string —
padding string —
borderColor string —
draggable boolean —
className string —

事件:@click。暴露 marker。

<TdtLabel :position="[116.42, 39.93]" text="天地图 4.0" :font-size="14" color="#d03050" background-color="rgba(255,255,255,.9)" padding="2px 6px" />
TdtTileLayer
Prop 类型 默认 说明
url string — 自定义瓦片地址(与 baseLayer 二选一)
baseLayer BaseLayerName vec 官方底图图层名
projection 'c' | 'w' w 官方底图投影
baseOptions BaseLayerOptions — 底图构造参数
options TileLayerOptions — 图层构造参数
visible boolean true 是否显示
opacity number — 透明度(响应式更新)

暴露 layer。tk 自动从 <TdtMap> 上下文获取。

<TdtMap :tk="TK">
  <!-- 叠加影像注记 -->
  <TdtTileLayer base-layer="cia" projection="w" :opacity="0.9" />
  <!-- 自定义瓦片服务 -->
  <TdtTileLayer url="https://tile.example.com/{z}/{x}/{y}.png" :options="{ maxZoom: 18 }" />
</TdtMap>
TdtControl

把任意 DOM 放进一个自动按 position 停靠的控件里。

Prop 类型 默认
position ControlPosition topright
offset [number, number] —
visible boolean true

暴露 control。

<TdtControl position="bottomright" :offset="[0, -10]">
  <div class="card">
    <button @click="locate">定位</button>
  </div>
</TdtControl>

3.13 Vue 3 组合式函数

useTianditu(options) —— 加载脚本
import { useTianditu } from '@bonncloud/tianditu/vue'

const { T, isLoading, error, load } = useTianditu({
  tk: 'YOUR_BROWSER_TK',
  immediate: true,        // 默认 true,在 onMounted 中自动加载
})

// 手动加载(幂等)
await load()

返回:T、isLoading、error、load。

useTdtMap(containerRef, options, provideToDescendants?) —— 创建地图
import { ref } from 'vue'
import { useTdtMap } from '@bonncloud/tianditu/vue'

const container = ref<HTMLElement | null>(null)
const { map, T, ready, isLoading, error, init, provideContext } = useTdtMap(container, {
  tk: 'YOUR_BROWSER_TK',
  center: [116.4, 39.9],
  zoom: 12,
  immediate: true,        // 默认 true,onMounted 自动初始化
})

center 与 zoom 是响应式的,变化会自动同步到地图。组件卸载(onScopeDispose)时自动 destroy()。

<template>
  <div ref="container" style="height: 400px" />
</template>
useTiandituOverlay(create, deps, hooks) —— 自定义覆盖物生命周期

把任意覆盖物绑定到父级 <TdtMap> 提供的地图实例上:地图就绪后自动创建、依赖变化自动重建、组件卸载自动移除。

import { useTiandituOverlay } from '@bonncloud/tianditu/vue'
import { createPolyline } from '@bonncloud/tianditu'

const props = defineProps<{ points: [number, number][] }>()

const { overlay, refresh, remove } = useTiandituOverlay(
  ({ T }) => createPolyline(T, props.points, { color: '#165dff', weight: 4 }),
  [() => props.points],                                   // 依赖变化时重建
  {
    events: { click: (e) => console.log('点击折线', e) },  // 自动绑定事件
    onCreated: (instance, { map }) => console.log('已添加到地图', map.getZoom()),
  },
)

返回:overlay(Ref<O | null>)、refresh()、remove()。

useTiandituServices(options) —— Web 服务响应式封装
import { useTiandituServices } from '@bonncloud/tianditu/vue'
import { SearchQueryType } from '@bonncloud/tianditu'

const { client, loading, error, search, geocode, reverseGeocode, drive, transit, administrative, run } =
  useTiandituServices({
    tk: 'injected-by-server',      // 经转发端点时传占位符
    baseURL: '/api/tianditu',
    timeout: 10_000,
  })

// 方法出错时不抛出,而是返回 undefined 并把错误放进 error
const res = await search({
  queryType: SearchQueryType.Nearby,
  keyWord: '公园',
  pointLonlat: '116.48016,39.93136',
  queryRadius: 2000,
})
if (!res) console.log(error.value)

// run() 用于调用 client 上的任意方法(如 raw)
const custom = await run((c) => c.raw('/staticimage', { width: 400 }))

返回:client(ComputedRef<TiandituClient>)、loading、error、search、geocode、reverseGeocode、drive、transit、administrative、run。

TIANDITU_MAP_CONTEXT —— 自定义组件的注入键
import { inject } from 'vue'
import { TIANDITU_MAP_CONTEXT } from '@bonncloud/tianditu/vue'

const ctx = inject(TIANDITU_MAP_CONTEXT, null)
// ctx.T / ctx.map / ctx.ready / ctx.tk

ctx.map.value 为 TiandituMap,ctx.T.value 为 T 命名空间。用它可以写出自己的 <TdtXxx> 组件。


3.14 类型导出

// 服务端与客户端配置、请求/响应结构
import type {
  TiandituClientOptions, SearchParams, SearchResponse, SearchPoi,
  GeocodeParams, GeocodeResponse, ReverseGeocodeParams, ReverseGeocodeResponse,
  DriveParams, DriveResponse, DriveRoute, DriveStep,
  TransitParams, TransitResponse, TransitLine, TransitSegment,
  AdministrativeParams, AdministrativeResponse, AdministrativeItem,
  StaticMapParams, CoordSystem, ServiceStatus,
} from '@bonncloud/tianditu/types'

// 枚举
import { SearchQueryType, DriveStyle, BusLineType, AdminChildLevel } from '@bonncloud/tianditu/types'

// 浏览器端类型
import type {
  TiandituNamespace, MapOptions, MarkerOptions, PolylineOptions, PolygonOptions,
  CircleOptions, RectangleOptions, InfoWindowOptions, TileLayerOptions,
  IconOptions, DivIconOptions, TextMarkerOptions, MapTypeItem,
  LngLat, Point, LngLatBounds, Map, Overlay, Control,
  LngLatLike, PointLike, LngLatTuple, LineStyle,
  TiandituEvent, MapMouseEvent, MapEventMap, OverlayEventMap,
} from '@bonncloud/tianditu/types'

LngLatLike 覆盖四种输入:[lng, lat]、{ lng, lat }、{ lon, lat }、已有 T.LngLat。PointLike 支持 [x, y] 与 { x, y }。


3.15 完整可运行示例

Node.js 后端:搜索 + 地理编码 + 路径规划
import { TiandituClient, SearchQueryType } from '@bonncloud/tianditu/services'

const client = new TiandituClient({ tk: process.env.TIANDITU_SERVER_TK!, timeout: 8000, retry: 1 })

const pois = await client.search({
  queryType: SearchQueryType.Nearby,
  keyWord: '公园',
  pointLonlat: '116.48016,39.93136',
  queryRadius: 3000,
  start: 0,
  count: 20,
})
console.log(`找到 ${pois.count} 个结果,首个:${pois.pois?.[0]?.name}`)

const geo = await client.geocode({ keyWord: '北京市海淀区莲花池西路28号' })
const addr = await client.reverseGeocode({ lon: 116.37304, lat: 39.92594 })
console.log(geo.location?.lon, geo.location?.lat, '→', addr.result?.formatted_address)

const drive = await client.drive({ orig: '116.35,39.92', dest: '116.39,39.90' })
console.log(`全程 ${drive.distance} 公里 / ${drive.duration} 秒`)

const imgUrl = client.staticMapUrl({ center: '116.40,39.93', zoom: 12, width: 800, height: 600 })
console.log(imgUrl)
浏览器:命令式 API
import {
  loadTianditu, TiandituMap,
  createMarker, createTextMarker, createPolyline, createPolygon,
  createZoomControl, createScaleControl, createBaseLayer,
  drawShape, measureDistance, formatDistance,
} from '@bonncloud/tianditu'

const TK = import.meta.env.VITE_TIANDITU_TK

const T = await loadTianditu({ tk: TK })

const map = await TiandituMap.create({
  tk: TK,
  container: '#map',
  center: [116.40769, 39.89945],
  zoom: 12,
})

map.addControl(createZoomControl(T, { position: 'topleft' }))
map.addControl(createScaleControl(T, { position: 'bottomleft' }))
map.addLayer(createBaseLayer(T, TK, 'cia', { projection: 'w' }))   // 叠加影像注记

map.addOverlay(createMarker(T, [116.4, 39.9], { title: '北京' }))
map.addOverlay(createTextMarker(T, [116.42, 39.93], { text: '文字标注', style: { color: '#d03050' } }))
map.addOverlay(createPolyline(T, [[116.3, 39.9], [116.5, 39.9]], { color: '#165dff', weight: 4 }))
map.addOverlay(createPolygon(T, [[116.3, 39.85], [116.45, 39.85], [116.45, 39.95]], { fillColor: '#00b42a' }))

const off = map.on('click', (e) => console.log(e.lnglat?.getLng(), e.lnglat?.getLat()))

// 交互式测距
const pts = await drawShape(T, map.raw)
if (pts.length > 1) console.log(`总长 ${formatDistance(measureDistance(T, map.raw, pts))}`)

map.setBaseMap('satellite')
off()
map.destroy()
Vue 3:组件 + 组合式函数 + 转发调用 Web 服务
<script setup lang="ts">
import { ref } from 'vue'
import {
  TdtMap, TdtMarker, TdtPolyline, TdtPolygon, TdtCircle, TdtLabel, TdtInfoWindow, TdtControl,
  useTiandituServices,
} from '@bonncloud/tianditu/vue'
import { SearchQueryType, type TiandituMap } from '@bonncloud/tianditu'

const TK = import.meta.env.VITE_TIANDITU_TK        // 浏览器端 Key:可供地图渲染

// Web 服务不能由浏览器直连,baseURL 指向自有转发端点,前端不持有服务端 Key
const { search, reverseGeocode, loading, error } = useTiandituServices({
  tk: 'injected-by-server',
  baseURL: '/api/tianditu',
})

const center = ref<[number, number]>([116.40769, 39.89945])
const map = ref<TiandituMap | null>(null)
const show = ref(true)
const pois = ref<{ name: string; position: [number, number] }[]>([])

async function doSearch() {
  const res = await search({
    queryType: SearchQueryType.Nearby,
    keyWord: '公园',
    pointLonlat: center.value.join(','),
    queryRadius: 3000,
  })
  pois.value = (res?.pois ?? []).map((p) => {
    const [lng, lat] = p.lonlat.split(',').map(Number)
    return { name: p.name, position: [lng, lat] as [number, number] }
  })
}
</script>

<template>
  <TdtMap
    :tk="TK"
    :center="center"
    :zoom="12"
    :controls="{ zoom: true, scale: { position: 'bottomleft' }, mapType: true }"
    @ready="map = $event"
  >
    <TdtMarker v-for="poi in pois" :key="poi.name" :position="poi.position" :title="poi.name" />

    <TdtPolyline :points="[[116.3, 39.9], [116.5, 39.9]]" color="#165dff" :weight="4" />
    <TdtPolygon :points="[[116.3, 39.85], [116.45, 39.85], [116.45, 39.95]]" fill-color="#00b42a" />
    <TdtCircle :center="center" :radius="1500" fill-color="#ff7d00" />
    <TdtLabel :position="[116.42, 39.93]" text="标注文字" />

    <TdtInfoWindow v-if="show" :position="center" content="<b>标题</b><br/>内容" />

    <TdtControl position="bottomright">
      <button @click="doSearch">搜索周边</button>
    </TdtControl>
  </TdtMap>
</template>
浏览器接入 Web 服务:后端转发

前端不直连天地图(原因见 4.3),把 baseURL 指向自己的转发端点即可:

浏览器 ──/api/tianditu/**──▶ 你的后端 ──▶ https://api.tianditu.gov.cn/**
                            (覆盖 tk、不转发 Referer/Origin)
// 前端:tk 传占位符,转发层会覆盖成真实 Key
const client = new TiandituClient({ tk: 'injected-by-server', baseURL: '/api/tianditu', timeout: 10_000 })
const { search } = useTiandituServices({ tk: 'injected-by-server', baseURL: '/api/tianditu' })
const loc = await jsApi.geocode(client, '北京市海淀区莲花池西路28号')   // jsApi 直接接受客户端实例
// 后端:三件事 —— 覆盖 tk、不转发 Referer/Origin、原样回传响应体
// 可直接复用的实现见 examples/vue3-demo/server/tiandituProxy.ts
// (开发期以 Vite 插件形态提供,其中的 handleProxy() 可原样挂到 Express / Koa)
开发与联调
pnpm install
pnpm typecheck                  # vue-tsc 类型检查
pnpm test                       # vitest,41 项用例
pnpm build                      # 产出 dist(ESM + CJS + d.ts + sourcemap)
node scripts/live-check.mjs     # 逐个打真实接口(需在 examples/vue3-demo/.env 配置服务端 Key)

# 运行 Vue 3 示例
cd examples/vue3-demo
pnpm install
pnpm dev                        # http://localhost:5177,同时启用 /api/tianditu 转发

示例工程的 .env 需要两个 Key(类型不同,权限不通用):

变量 前缀 用途
VITE_TIANDITU_TK 必须带 VITE_ 浏览器端 Key,给 <TdtMap> 渲染底图
TIANDITU_SERVER_TK 不要带 VITE_ 服务端 Key,只在 Node 侧读取,由转发中间件注入

带 VITE_ 前缀的变量会被 Vite 内联进前端产物、随页面公开 —— 服务端 Key 绝不能加这个前缀。


4. 注意事项

4.1 JS API 4.0 的能力边界(最重要)

实测加载 https://api.tianditu.gov.cn/api?v=4.0(返回版本 4.1.6)后,全局 T 上可用的公开类为:

Map  LngLat  Point  Bounds  LngLatBounds  Icon  DivIcon  Marker  Polyline
Polygon  Circle  Rectangle  InfoWindow  TileLayer  MapType  Overlay
LayerGroup  GeoJSON  Version
T.Control:  Zoom  Scale  MapType  Copyright

此外主脚本上还存在 Protocol ScriptLoader extend setOptions 与 T.Control.DataSources,但本包未为其提供类型封装,需要时走 (T as any).xxx。

不存在的类(文档站有页面但运行时 undefined)及其替代方案见 2.3。官方文档站 api/js4.0/pages-class/ 下的 Label.html 与 MarkerClusterer.html 属遗留页面:在主脚本(138 KB)中检索这两个名字均出现 0 次,聚合库 opensource/openlibrary/MarkerClusterer.js 也是 404。

4.2 两种 Key 权限不通用

在天地图控制台创建应用时要选应用类型,两类 Key 权限完全隔离:

Key 类型 可用于 用在 Web 服务 API 会得到
浏览器端 JS API 4.0(网页里嵌地图) 403 + {"code":301012,"msg":"权限类型错误"}
服务端 Web 服务 API(搜索/地理编码/路径规划…) 可用

做完整功能请建两个应用、拿两个 Key:一个浏览器端给 <TdtMap :tk>,一个服务端给 TiandituClient / useTiandituServices / jsApi。

4.3 浏览器不能直连 Web 服务,必须走转发

服务端 Key 会校验请求来源:从 Node 调用不带 Referer 时正常,而带浏览器 Referer / Origin 的请求会被拒:

403 {"code":301013,"resolve":"Key权限类型为:服务器端,请使用服务器端访问!"}

这不是 CORS 配置缺失(响应头里有 Access-Control-Allow-Origin: *),而是业务层的来源校验,因此前端无法直连。请在自有后端用 TiandituClient 包一层接口给前端,或把 baseURL 指向转发端点(见 3.15)。

4.4 坐标系

  • 天地图底图基于 CGCS2000,与 WGS84 差异在厘米级,日常开发可直接等同。
  • 与高德 / 腾讯(GCJ02)、百度(BD09)的数据混用前,先用内置坐标工具互转,否则会有几百米级的偏移。
  • 天地图服务端接口的坐标一律是 "lng,lat" 顺序(经度在前),范围是 "minx,miny,maxx,maxy"。

4.5 单位与返回值

  • 驾车规划全程距离是「公里」(drive.distance / jsApi.drivingRoute().distance),而按段距离是「米」(route.steps[].distance)。同一份响应混用两种单位。
  • 官方 <routes>/<item> 里其实没有 streetDistance 字段,段里程只写在指引文本里,SDK 会解析出来(解析不到时为 NaN / 空串)。
  • 公交规划的 resultCode 中 1 2 4 5 属于「有提示但可用」的返回,3(规划失败)与 6(参数错误)才抛错。
  • 行政区划 V2.0 的 status 为 200 只代表请求成功,查无结果时 data.district 为空数组且不抛错; 条目字段是 gb / pgb / center: { lng, lat } / boundary,不存在 V1.0 的 lnt / cityCode / returncode。

4.6 容器与初始化

  • 原生 new T.Map() 只认不带 # 的元素 id或 DOM 元素,传 '#map' 会抛 Cannot read properties of null (reading '_tdt_events')。本包已归一化,但请确保容器在创建地图时已存在且有尺寸。
  • 容器尺寸变化后(侧边栏开合、标签页切换、弹窗打开)务必调用 map.resize(),否则地图会显示不全。
  • 使用 Vue 组件时,<TdtMap> 的父容器需要有明确高度(组件默认 height: 100%)。

4.7 框架与环境

  • SSR / Node:loadTianditu() 在无 document 的环境会立即抛出明确错误,请在 onMounted / useEffect 等客户端生命周期中调用;Web 服务客户端前后端通用,可放心在 Node 中使用。
  • fetch:Node.js < 18 无内置 fetch,通过 new TiandituClient({ fetch }) 注入实现(如 node-fetch)。
  • Vue:vue@^3.3 是可选 peer 依赖,只有引入 @bonncloud/tianditu/vue 时才需要。

4.8 配额与合规

  • 天地图按日限制调用量(例如个人开发者地图搜索 3000 次/日、地理编码 7000 次/日等,以官方最新公布为准),超限会返回对应错误码,生产环境请做好缓存与降级。
  • 静态地图单边最大 1024;周边搜索半径最大 10 公里;搜索结果 start / count 上限 300。
  • 请勿把服务端 Key 写进前端代码或前端环境变量(VITE_ / NEXT_PUBLIC_ 等前缀会自动内联进产物)。

4.9 已知小问题

  • TiandituMap.createSync(container) 的第二个参数 options 类型为 CreateMapOptions(其中 container 是必填项),实际上只读取 center 与 zoom。传入其它字段不会生效;如需完整配置请使用 TiandituMap.create()。
  • <TdtInfoWindow> 声明了 @close 事件,但当前版本尚未触发(天地图信息窗口关闭由地图侧 closeInfoWindow() 触发),如需感知关闭请自行监听 map.raw 上的事件。
  • 文字标注的 style 会把 camelCase 键转成 kebab-case,webkit 前缀属性会缺失开头的 -:传 webkitTextStrokeColor 生成的是 webkit-text-stroke-color:#xxx(浏览器会忽略该声明)。因此 <TdtLabel> 的 borderColor 描边目前不生效;如需描边,请在渲染后用 CSS 自行覆盖 .tdt-text-marker。
  • 本 README 中的截图位于 docs/screenshot.png,docs/ 目录不在 package.json 的 files 白名单内,因此在 npm 包页面上该图不会显示(仓库内正常)。如需在 npm 上展示,请在 files 中补上 docs/screenshot.png。
  • 实测服务端结论:用真实服务端 Key 跑 node scripts/live-check.mjs 的结果为 13/13 通过。 0.1.x 时行政区划步骤恒失败(CloudWAF 返回 418),当时判断为「服务端侧拦截,需检查该服务是否已开通」, 实际原因是 SDK 打的是已于 2024-06-30 下线的 V1.0 端点 GET /administrative —— 该路径现在即便不带任何参数也返回 418,属路径级失效,而非权限未开通。0.2.0 改用 GET /v2/administrative 后已恢复正常。

5. 版权

5.1 本包许可

本包基于 MIT License 发布,版权归 Bonncloud 所有:

MIT License

Copyright (c) 2026 Bonncloud

完整条款见 LICENSE。你可以在保留版权声明与许可声明的前提下,自由使用、修改、合并、发布、分发、再许可及销售本软件。

本包按「原样」提供,不附带任何形式的担保。

5.2 天地图服务与数据

本包只是天地图官方 API 的客户端封装,不包含、不代理、不转售任何地图数据或服务:

  • 天地图平台及其地图数据、瓦片、检索与规划服务的权利归国家基础地理信息中心(天地图)所有,使用前请自行申请服务许可 Key。
  • 使用天地图服务须遵守《天地图服务条款》《天地图 API 使用许可》及国家测绘地理信息相关法律法规,请以 天地图官网 公布的最新条款为准。
  • 请勿滥用接口、绕过配额限制,或把天地图数据用于条款禁止的用途。
  • 基于天地图成果制作并公开发布的地图,应按天地图要求标注数据来源与地图审图号(具体标注格式与审图号以天地图官方公布为准);SDK 提供了 createCopyrightControl() 便于在地图上添加版权标注。
  • 服务可用性、配额、接口响应与字段定义均由天地图官方决定,可能随版本变更,本包不对此作出承诺。

5.3 第三方

  • 本包无运行时第三方依赖。
  • 开发期依赖(Vite、TypeScript、Vitest、Vue 等)各自遵循其开源许可。
  • 官方开源扩展库(HeatmapOverlay、CarTrack、D3SvgOverlay、BufferTool、ImageOverlay)由天地图以 CDN 形式提供,需通过 loadPlugin() 单独加载,其版权归原作者所有,不在本包许可范围内。

相关文件

文件 说明
CHANGELOG.md 版本变更记录,含实测结论与破坏性变更说明
LICENSE MIT 许可全文
examples/vue3-demo Vue 3 + Vite 全功能示例工程(含 Web 服务转发中间件)
scripts/live-check.mjs 真实接口联调自检脚本

Related Packages

@movk/maplibre

声明式 MapLibre GL 封装库:MaplibreMap/Source/Layer 等组件与 composables,原生支...

1,028 0
my-openlayer

my-openlayer 是一个基于 OpenLayers 的现代地图组件库,专为 Web GIS 应用开发者设计...

9,722 9
vue-tianditu

vue-tianditu

7,691 123

Version History

Version Released Vue License
0.2.0 ^3.3.0 MIT
0.1.3 ^3.3.0 MIT
0.1.2 ^3.3.0 MIT