引入方式
一个 <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>
<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) | 逆地理,别名 regeocode;opts.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()
事件
| 事件 | 回调参数 |
|---|---|
load | map 实例 |
click / dblclick | { lng, lat, crs, originalEvent } |
mousemove | 同上 |
centerchange | { lng, lat } |
zoomchange | { zoom } |
moveend | { center, zoom, bounds } |
basemapchange | { id, name } |
markeradd | marker 实例 |
route | 路线对象 |
popupopen | 弹窗参数 |
error | Error 对象 |
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
自有瓦片 / 自有 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') 注销。
同名重复注册视为改配置,已建好的图层会自动重建。
?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.list、data.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 公里)时,
等于把用户扔到一条完全不相干的街道上 —— 看起来很精确,其实是错的。
所以组件会按下面顺序决定级别:
- 给了
radius:按这个半径算,让「周边」刚好铺满视口短边 - 定位成功且
fitAccuracy(默认开):按accuracy的 1.5 倍算,精度差就自动缩远 - 都没有:用
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。
fetch 与瓦片全被拦时,curl 里看全是 200 —— 极具迷惑性。
本服务已在 /api、/vendor、/shared、/sdk 四个前缀开放。