引入方式

一个 <script> 就够了 —— 无构建、无 CDN、无需 import map,第三方站点可直接引。

<!-- 最简形式:带上你的 Key -->
<script src="https://your-host/sdk/oms.js?key=YOUR_KEY"></script>

<!-- 对齐腾讯/百度的「回调自动初始化」用法 -->
<script src="https://your-host/sdk/oms.js?key=YOUR_KEY&callback=initMap"></script>

<!-- 服务不在同源(反代到别的路径)时,显式指定根地址 -->
<script src="https://cdn.example.com/sdk/oms.js?base=https://map.example.com"></script>
服务根地址是从 SDK 自己那个 <script>src 反推的, 不是取 location.origin —— 这样跨域引入或反代到别的路径时都指向正确。 要覆盖请用 URL 上的 ?base=不支持运行时用 OMS.load({base}),因为 OpenLayers、坐标模块和底图列表在脚本执行那一刻就已开始异步加载, 事后改只会造成「一半资源来自老地址」的隐蔽错乱。

为什么用 ?key= 而不是请求头

瓦片是 <img> 标签加载的,带不了请求头。如果接口用 header、瓦片用 query, 就会出现「接口能调、底图 401」的一半可用状态。所以 Key 统一走查询参数。

快速开始

<div id="map" style="width:100%;height:420px"></div>

<script src="https://your-host/sdk/oms.js?key=YOUR_KEY"></script>
<script>
OMS.ready(function () {
  var map = new OMS.Map('map', {
    center: [116.397, 39.909],   // [lng, lat],WGS84
    zoom: 12,
  });

  // 搜索并标注
  OMS.search('天安门').then(function (r) {
    map.addMarker({ lng: r.lng, lat: r.lat, name: r.name });
    map.setCenter(r.lng, r.lat);
  });

  // 规划并绘制路线
  map.route([116.397, 39.909], [116.316, 39.983], { profile: 'car' });
});
</script>

就绪与生命周期

SDK 会异步加载 OpenLayers、坐标模块与底图列表。两种等法等价:

OMS.ready(function (OMS) { /* 回调 */ })
await OMS.ready()            // 返回 Promise
await OMS.load({ key: '…' }) // 顺带设置 Key
ready 之前调用 OMS.coord.*OMS.tiles.sources() 会抛错。 就绪是三样资源全部就绪才算数,避免出现「地图能用但坐标模块还在加载」这种一半可用状态。

坐标系

SDK 对外默认 WGS84(不是腾讯/百度的 GCJ02)。三种处理位置:

// 1) 传 crs 让服务端换算(推荐,只转一次)
OMS.search('天安门', { crs: 'GCJ02' })

// 2) 本地纯算法转换,零网络往返
OMS.coord.convert([116.397, 39.909], 'WGS84', 'GCJ02')
OMS.coord.convert({ lng: 116.397, lat: 39.909 }, 'WGS84', 'BD09')

// 3) 走服务端接口(结果一致,多一次请求)
await OMS.coord.server({ lng: 116.397, lat: 39.909 }, 'GCJ02')
不要客户端转一遍、服务端再转一遍 —— 会双重偏移。 把原始坐标系用 crs 告诉服务端,转换只发生一次。

全局方法

方法说明
OMS.Map(container, opts)创建地图实例
OMS.ready(fn) / await就绪回调或 Promise
OMS.load({key, callback})设置 Key 并等待就绪
OMS.key(k)运行时设置/覆盖 Key
OMS.search(q, opts)关键词搜索,别名 geocode
OMS.suggest(q, opts)输入联想,只回名称与坐标
OMS.reverseGeocode(lng, lat, opts)逆地理,别名 regeocodeopts.mode='local' 只走离线库(毫秒级,不等上游)
OMS.route(from, to, opts)路线规划,返回路线对象(不画图)
OMS.route.profiles()可用的出行方式
OMS.coord.convert(v, from, to)坐标系互转
OMS.coord.all(v, from)一次拿到全部坐标系表示
OMS.coord.local.*本地算法直调(wgs84ToGcj02 等)
OMS.tiles.sources()可用底图列表(内置 + 自定义)
OMS.tiles.url(id, z, x, y)拼出单张瓦片 URL
OMS.tiles.register(id, opts)注册自有 XYZ 瓦片源
OMS.tiles.unregister(id)注销自有瓦片源
OMS.tiles.custom()只看已注册的自定义源
OMS.poi.use(endpoint, opts)改用自有 POI 检索服务
OMS.poi.clear() / OMS.poi.endpoint()恢复内置检索 / 查看当前配置
OMS.health()服务健康状态

搜索的额外参数

OMS.search('餐厅', {
  center: [116.397, 39.909],   // 周边搜索中心
  radius: 2000,                // 半径(米)
  limit: 20,
  crs: 'WGS84',
})

OMS.search('', {
  polygon: '116.3,39.8;116.5,39.8;116.5,40.0;116.3,40.0',  // 分号分隔,不是 JSON
})

Map 实例方法

方法说明
whenReady(fn)地图初始化完成回调
getCenter(crs) / setCenter(lng, lat, crs)中心点读写
getZoom() / setZoom(z)缩放级别
getBounds()当前视口范围
fitBounds(bounds, opts)自适应到指定范围
setBasemap(id) / getBasemap() / getBasemaps()底图切换
addMarker(opts) / addMarkers(list)加标注
getMarker(id) / removeMarker(m) / clearMarkers()标注管理
route(from, to, opts)规划并绘制路线(别名 showRoute
getRoute() / clearRoute()路线读写与清除
openPopup(opts) / closePopup()信息窗
on(type, fn) / off(type, fn)事件绑定
resize() / destroy()尺寸刷新与销毁

Marker

var m = map.addMarker({
  lng: 116.397, lat: 39.909,
  name: '天安门',
  icon: 'pin',          // pin / dot / star / flag / target
  color: '#d33a2c',
  crs: 'WGS84',
  popup: '<div>自定义内容</div>',   // 或 { title, body }
})

m.setPosition(116.40, 39.91)
m.openPopup()
m.remove()

事件

事件回调参数
loadmap 实例
click / dblclick{ lng, lat, crs, originalEvent }
mousemove同上
centerchange{ lng, lat }
zoomchange{ zoom }
moveend{ center, zoom, bounds }
basemapchange{ id, name }
markeraddmarker 实例
route路线对象
popupopen弹窗参数
errorError 对象
map.on('click', function (e) {
  console.log(e.lng, e.lat, e.crs)
})
监听器里抛出的异常会被重新抛到全局setTimeout(() => { throw e }, 0))。 不这么做的话,你在回调里写错一行只会表现为「地图出来了但后面全没执行」,几乎无法定位。

底图

var list = await OMS.tiles.sources()
// [{ id, name, maxZoom, attribution, description, kind, badge, proxyUrl }]

map.setBasemap('osm')      // 按 id 切换
map.getBasemap()           // 当前 id
底图来自 OpenStreetMap 等开源瓦片服务,必须保留页面上自动渲染的署名信息(ODbL 要求)。

自有瓦片 / 自有 POI

有自己的瓦片服务或自己的 POI 库时,不必把数据迁进来 —— 两处都可以直接接上,且内置底图与内置检索不会被顶掉,是「多一个源」而不是「换掉源」。

一、注册自有瓦片

OMS.tiles.register('my-tiles', {
  url: 'https://tiles.example.com/{z}/{x}/{y}.png',  // 必须含 {z}/{x}/{y}
  name: '我的瓦片',
  attribution: '© 某某公司',
  maxZoom: 18,
  // tileSize: 512,        // 非 256 时必填,否则层级会错位
  // crossOrigin: null,    // 默认不发,见下方说明
  // params: { key: '…' }, // 对方要鉴权就放这里
})

new OMS.Map('map', { basemap: 'my-tiles' })   // 也可以 map.setBasemap('my-tiles')

注册后与内置底图完全同级:出现在 OMS.tiles.sources() 里(且排在前面), 可以 setBasemap 切过去,也可以 OMS.tiles.unregister('my-tiles') 注销。 同名重复注册视为改配置,已建好的图层会自动重建。

两个安全默认值:① 自定义瓦片 URL 不会拼上你的 ?key=(那是第三方地址,key 出去了就收不回来),要鉴权请用 params; ② 默认不加 crossOrigin —— 对方没开 CORS 时设 'anonymous' 会让瓦片全部加载失败,只有确需 canvas 导出像素时才设它。

二、接上自有 POI 检索

// 字符串 = 基地址,SDK 自行拼 /search、/suggest、/reverse
OMS.poi.use('https://api.example.com/poi')

// 也可以逐项指定完整地址;某项不填,该能力仍走内置检索
OMS.poi.use({
  search: 'https://api.example.com/poi/query',
  suggest: 'https://api.example.com/poi/hint',
})

// 对方返回结构不一样?用 adapter 自己转
OMS.poi.use('https://api.example.com/poi', {
  adapter: function (body) {
    return body.data.list.map(function (x) {
      return { name: x.title, lng: x.location[0], lat: x.location[1], address: x.addr }
    })
  },
  params: function (p) { p.token = 'abc'; return p },   // 改参数名 / 补鉴权
  headers: { Authorization: 'Bearer abc' },
  fallback: true,     // 默认 true:接口挂了回退内置检索并 warn
})

OMS.search('西湖')    // 之后 search / suggest / reverseGeocode 全部走上面这个地址
OMS.poi.clear()       // 恢复内置检索

不传 adapter 时会按常见结构自动猜测:结果数组认 results / data / items / list (含 data.listdata.results),坐标认 lng+lat / lon+lat / location。猜不出会 warn 并返回空数组。

自定义接口返回的结果里 source'custom'OMS.search(q, { full: true }) 还会带 custom: true,便于区分数据来自哪一路。

三、引脚本时一次带齐

<!-- 值必须 URL 编码,否则 {z} 会被浏览器吃掉 -->
<script src="https://your-host/sdk/oms.js?tiles=https%3A%2F%2Ftiles.example.com%2F%7Bz%7D%2F%7Bx%7D%2F%7By%7D.png&tilesName=%E6%88%91%E7%9A%84%E7%93%A6%E7%89%87&poi=https%3A%2F%2Fapi.example.com%2Fpoi"></script>
<!-- 可选:&tilesZoom=18(最大层级) -->

?tiles= 会注册成 id 为 custom 的底图,并自动设为默认底图 —— 引了自己的瓦片还默认用别人的底图是最反直觉的行为。 ?poi= 等价于 OMS.poi.use(...)

?tiles= 给的模板必须含 {z}/{x}/{y},否则 SDK 会在控制台报错并忽略该参数 (而不是造出一张永远白屏的图层)。

路线规划

// 只算不画
var r = await OMS.route([116.397, 39.909], [116.316, 39.983], { profile: 'car' })

// 一步到位:算完画线并把视野调过去
await map.route([116.397, 39.909], [116.316, 39.983], { profile: 'bike' })

map.clearRoute()

profile 取值 car / bike / foot。 也支持对象形式:OMS.route({ from: […], to: […], profile: 'foot' })

路网数据缺少路口名称与单行道属性,不提供路口级转向指引, 只保证几何路径与距离/耗时估算正确。

地图选点(带当前定位)

「让用户挑一个位置,把坐标和地址带回来」——这是第三方站点最常见的用法。 SDK 提供开箱即用的选点组件:可以携带当前定位打开,并 按定位精度自动决定缩放级别,直接展示合适的周边范围。

一、一行式:弹层选点

// 自动用浏览器定位打开;确认 resolve 结果,取消 resolve null(不会 reject)
var res = await OMS.chooseLocation({ title: '选择收货地址' })
if (!res) return

// res = { lng, lat, crs:'WGS84', gcj02, bd09, address, name, city, zoom, accuracy, source }
console.log(res.address, res.lng, res.lat)

二、嵌入自己的容器

<div id="box" style="width:100%;height:420px"></div>

var picker = OMS.pickLocation('box', {
  center: [116.4033, 39.9165],  // 携带当前定位(宿主页面拿到的那个点)
  crs: 'GCJ02',                 // 告诉 SDK 这个点是什么坐标系,内部统一换算
  zoom: 16,                     // 没定位/没精度时的默认级别
  radius: 800,                  // 或:明确「要 800 米周边」,按半径定级
})

picker.on('pick', function (res) { /* 用户点了确认 */ })
picker.on('change', function (res) { /* 拖动中,地址已刷新 */ })
picker.on('locate', function (e) { /* 定位成功,e.accuracy / e.zoom */ })
picker.on('locateerror', function (e) { /* e.code: GEO_DENIED / GEO_TIMEOUT … */ })

picker.destroy()   // 记得在页面卸载/关闭浮层时调用

三、缩放级别怎么定的

固定 zoom=16 在 GPS 准的时候刚好,但在基站定位(误差 2 公里)时, 等于把用户扔到一条完全不相干的街道上 —— 看起来很精确,其实是错的。 所以组件会按下面顺序决定级别:

  1. 给了 radius:按这个半径算,让「周边」刚好铺满视口短边
  2. 定位成功且 fitAccuracy(默认开):按 accuracy 的 1.5 倍算,精度差就自动缩远
  3. 都没有:用 zoom,默认 16
// 精度 1500m → 约 13 级;精度 50m → 约 17 级;两者都不会是写死的 16
picker.on('locate', function (e) { console.log(e.accuracy, '→', e.zoom) })
定位的三条硬约束
  • 浏览器定位只在安全上下文可用(HTTPS、localhost、127.0.0.1);HTTP 页面下 navigator.geolocation 直接不存在,组件会走 locateerror,不会静默失败。
  • navigator.geolocation 返回的就是 WGS84,与本项目底图一致 —— 不要再转一次 GCJ02,转了会偏 500 米。需要火星坐标请用结果里的 gcj02 字段。
  • 定位是异步的、还可能弹授权框,所以地图会先用传入的 center(或内置兜底点)出图,定位回来再飞过去 —— 不会让你先看到几秒白屏。

四、拖动选点的地址从哪来

拖动时每 350ms 逆地理一次,默认走离线库reverseMode:'local',毫秒级)。 这是刻意的:选点的地址是给人看的预览,而远程上游一旦不可达,每次拖动都要等满超时(好几秒), 体验会直接崩掉。想要远程精度请显式传 reverseMode:'auto'

方法一览

方法说明
OMS.chooseLocation(opts)弹层选点,Promise 返回结果 / null
OMS.pickLocation(el, opts)在容器内创建,返回 LocationPicker
OMS.locate(opts)只要一次浏览器定位,返回 {lng,lat,accuracy,…}
picker.getResult(crs)当前选中的结果;传 crs 可换算成 GCJ02 / BD09
picker.setCenter(lng,lat,crs)手动移动选点
picker.locate()重新定位并按精度定级
picker.confirm() / cancel()程序化确认 / 取消
picker.getMap()拿到内部 OMS.Map,可继续加标注、画路线
picker.destroy()销毁并移除 DOM

在线演示

下面就是实际的演示页(与 /sdk-demo.html 同一份),可以直接点着试:

在新窗口打开演示 →

注意事项

别用 new URL() 拼瓦片地址{z}/{x}/{y} 会被转义成 %7Bz%7D,OpenLayers 匹配不到占位符, 表现是底图一张不出且不报错,只有白屏。用字符串拼接 + 逐项 encodeURIComponent
跨域必须开 CORS: 脚本能加载但 fetch 与瓦片全被拦时,curl 里看全是 200 —— 极具迷惑性。 本服务已在 /api/vendor/shared/sdk 四个前缀开放。
OpenLayers 用 UMD 单文件: ESM 版依赖 import map 解析裸标识符,不能要求第三方页面为引我们的 SDK 去改自己页面的配置。
回调里的异常要查控制台:SDK 会把它们重抛到全局,不会静默吞掉。