必应地图地址
首先了解
必应地图的在线瓦片 url 地址的获取方法和国内图商不太一样,需要从影像元数据 API 请求得到,详细的官方使用教程在获取影像元数据和影像元数据,API 地址如下:
https://dev.ditu.live.com/REST/V1/Imagery/Metadata/{imagerySet}?mapLayer={mapLayer}&o=json&c=zh-cn&key={BingMapsKey}
- {imagerySet} 参数:元数据的影像类型,相关可用的值如下表
值 | 说明 |
---|---|
Aerial | 航拍图像 |
AerialWithLabelsOnDemand | 叠加道路的航空影像 |
CanvasDark | 黑色版本的世界电子地图 |
CanvasLight | 轻量版本的世界电子地图 |
CanvasGray | 灰色版本的世界电子地图 |
RoadOnDemand | 常规版本的中国电子地图 |
RoadVibrantDark | 黑色版本的中国电子地图 |
官网说,中国地区影像获取元数据的 url 的 imagerySet 参数仅支持 RoadOnDemand 和 RoadVibrantDark,但经过测试,上表中其他值传入也能得到对于的在线瓦片地址
- {mapLayer} 参数:获取的切片类型,可以组合赋值,如 mapLayer=Basemap,Buildings,相关可用的值如下表
值 | 说明 |
---|---|
Basemap | 常规地图切片,默认 |
Background | 仅地图背景(区域面) |
Foreground | 仅地图前景(路网和注记层) |
TrafficFlow | 实时路况信息 |
Buildings | 建筑立体图形 |
并不是任意组合都是有效的,下文会测试出那些组合是有效的。
- {BingMapsKey} 参数:微软必应地图密钥,可以在这里申请。
API 响应结果会包括 在线瓦片地址模板、子域列表、瓦片宽高、最大最小可见层级等。这里不多做介绍,可查阅官方教程。
地址获取
通过以下代码对影像元数据 API 进行遍历请求,
async function getAllUrl () {
let l = "https://dev.ditu.live.com/REST/V1/Imagery/Metadata/{imagerySet}?{mapLayer}o=json&c=zh-cn&key={BingMapsKey}"
let a = ["Aerial", "AerialWithLabelsOnDemand", "CanvasDark", "CanvasLight", "CanvasGray", "RoadOnDemand", "RoadVibrantDark"]
let b = ["", "Basemap", "Background", "Foreground", "TrafficFlow", "Buildings", "Basemap,Background", "Background,Foreground", "Foreground,TrafficFlow", "TrafficFlow,Buildings", "Basemap,Foreground", "Basemap,TrafficFlow", "Basemap,Buildings", "Background,TrafficFlow", "Background,Buildings", "Foreground,Buildings"]
let map = ["Basemap", "Background", "Foreground", "TrafficFlow", "Buildings"]
let ttt = []
function sleep (time) { return new Promise((resolve) => setTimeout(resolve, time)) }
async function pp (imagerySet, mapLayer) {
let metaData = await fetch(L.Util.template(l, { imagerySet: imagerySet, mapLayer: mapLayer === "" ? "" : "mapLayer=" + mapLayer + "&", BingMapsKey: "AmLYMlFcLiYKOL_0K304vMOqq18dTfeBlja7GoB_-2KJltJXDmIBU7vaHG-xyEm2" })).then(res => res.json())
if (metaData.statusCode === 200) {
var resource = metaData.resourceSets[0].resources[0]
let no = true
let fli = ttt.forEach(e => {
if (e.url === resource.imageUrl) {
no = false
e.imagerySet = e.imagerySet === imagerySet ? e.imagerySet : (e.imagerySet + " 或 " + imagerySet)
e.mapLayer = e.mapLayer + " 或 " + (mapLayer === "" ? "无" : mapLayer)
}
})
if (no) { ttt.push({ imagerySet: imagerySet, mapLayer: mapLayer === "" ? "不传" : mapLayer, url: resource.imageUrl }) }
console.dir(resource.imageUrl)
} else {
console.dir("请求元数据出错: " + metaData.statusCode + ":" + metaData.statusDescription)
}
}
for (let i = 0; i < a.length; i++) {
let imagerySet = a[i]
for (let j = 0; j < b.length; j++) {
await pp(imagerySet, b[j])
await sleep(10)
await pp(imagerySet, map.filter((e) => { return b[j].indexOf(e) < 0 }).join(",")) // 五个取 5 个种类 = 五个取 0 个种类,五个取 4 个种类 = 五个取 2 个种类,五个取 3 个种类 = 五个取 2 个种类
}
}
console.dir(ttt.map((e) => { return "|" + e.imagerySet + "|" + e.mapLayer + "|" + e.url + "|" }).join("\r\n"))
}
测试得出了45个地址不重复的瓦片地址。
imagerySet 参数 | mapLayer 参数 | 在线瓦片地址 | 备注 |
---|---|---|---|
Aerial | Basemap | //{subdomain}.ssl.ak.tiles.virtualearth.net/tiles/a{quadkey}.jpeg?g=13184 | |
AerialWithLabelsOnDemand | Basemap | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=A,G,L&og=767&n=z | |
AerialWithLabelsOnDemand | Foreground | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,L&og=767&n=z | |
AerialWithLabelsOnDemand | Background | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,L&og=767&n=z | |
AerialWithLabelsOnDemand | TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=A,G,TF,L&og=767&n=z | |
AerialWithLabelsOnDemand | Foreground,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,TF,L&og=767&n=z | |
AerialWithLabelsOnDemand | Background,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,TF,L&og=767&n=z | |
CanvasDark | Basemap | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,L&cstl=WD&og=767&n=z | |
CanvasDark | Foreground | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,L&cstl=WD&og=767&n=z | |
CanvasDark | Background | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB&cstl=WD&og=767&n=z | |
CanvasDark | TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,TF,L&cstl=WD&og=767&n=z | |
CanvasDark | Foreground,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,TF,L&cstl=WD&og=767&n=z | |
CanvasDark | Background,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,TF&cstl=WD&og=767&n=z | |
CanvasLight | Basemap | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,L&cstl=WL&og=767&n=z | |
CanvasLight | Foreground | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,L&cstl=WL&og=767&n=z | |
CanvasLight | Background | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB&cstl=WL&og=767&n=z | |
CanvasLight | TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,TF,L&cstl=WL&og=767&n=z | |
CanvasLight | Foreground,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,TF,L&cstl=WL&og=767&n=z | |
CanvasLight | Background,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,TF&cstl=WL&og=767&n=z | |
CanvasGray | Basemap | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,L&cstl=CG&og=767&n=z | |
CanvasGray | Foreground | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,L&cstl=CG&og=767&n=z | |
CanvasGray | Background | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB&cstl=CG&og=767&n=z | |
CanvasGray | TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,TF,L&cstl=CG&og=767&n=z | |
CanvasGray | Foreground,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,TF,L&cstl=CG&og=767&n=z | |
CanvasGray | Background,TrafficFlow | //{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,TF&cstl=CG&og=767&n=z | |
RoadOnDemand | Basemap | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,L&og=767&n=z | |
RoadOnDemand | Foreground | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,L&og=767&n=z | |
RoadOnDemand | Background | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB&og=767&n=z | |
RoadOnDemand | TrafficFlow | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,TF,L&og=767&n=z | |
RoadOnDemand | Foreground,TrafficFlow | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,TF,L&og=767&n=z | |
RoadOnDemand | Background,TrafficFlow | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,TF&og=767&n=z | |
RoadOnDemand | Basemap,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,BX,L&og=767&n=z | |
RoadOnDemand | Foreground,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,BX,L&og=767&n=z | |
RoadOnDemand | Basemap,TrafficFlow,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,BX,TF,L&og=767&n=z | |
RoadOnDemand | Foreground,TrafficFlow,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,BX,TF,L&og=767&n=z | |
RoadVibrantDark | Basemap | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Foreground | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Background | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB&cstl=VBD&og=767&n=z | |
RoadVibrantDark | TrafficFlow | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,TF,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Foreground,TrafficFlow | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,TF,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Background,TrafficFlow | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=GB,TF&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Basemap,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,BX,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Foreground,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,BX,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Basemap,TrafficFlow,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=G,BX,TF,L&cstl=VBD&og=767&n=z | |
RoadVibrantDark | Foreground,TrafficFlow,Buildings | //dynamic.{subdomain}.tiles.ditu.live.com/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&it=Z,GF,BX,TF,L&cstl=VBD&og=767&n=z |
官方说在线瓦片地址可能发生变化,所以每次使用时尽量通过请求元数据 API 来得到。
除开 Aerial 卫片影像地址,通过对请求参数和得到的电子地图地址进行规律摸索,得出以下结论:
- imagerySet 影响 cstl 参数。
RoadVibrantDark => cstl=VBD、CanvasGray => cstl=CG、CanvasLight => cstl=WL、CanvasDark => cstl=WD、AerialWithLabelsOnDemand 和 RoadOnDemand 没有 cstl 参数。- mapLayer 影响 it 参数。
it 是一个组合值,it 符合 (G|GB)[,BX][,TF][,L|,LA] 或者 (Z)([,GF]|[,BX]|[,TF]|[,L|,LA]) 模板,
即 (路网+区域面|区域面)[,立体建筑][,实时路况][,注记|,注记+POI] 或者 (开头符号表示无区域面)([,立体建筑][,实时路况][,注记|,注记+POI]) 。- 整理后的整个模版地址:(//{subdomain}.ssl.ak.dynamic.tiles.virtualearth.net|//dynamic.{subdomain}.tiles.ditu.live.com)/comp/ch/{quadkey}?mkt=zh-CN&ur=CN&og=767&n=z&it=((G|GB)[,BX][,TF][,L|,LA]|(Z)([,GF]|[,BX]|[,TF]|[,L|,LA]))[&cstl=(VBD|CG|WL|WD)],其中()内必填,[]内选填,|或关系。
Leaflet 添加代码
将 {imagerySet} 和 {mapLayer} 参数,用 | 隔开, 或 在线瓦片 url,即可将相关在线瓦片添加至 leaflet 预览
在线运行
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<style type="text/css">
body { padding: 0; margin: 0; }
html, body, #map { height: 100%; }
#param-test { position: absolute; z-index: 999; left: 10px; top: 10px; background: white; padding: 0 3px; line-height: 28px; font-size: 14px }
#url-input { width: 450px; height: 18px; margin-bottom: 3px; }
</style>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.2/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.2/dist/leaflet.js"></script>
<script src="https://unpkg.com/gcoord@0.3.2/dist/gcoord.js"></script>
</head>
<body>
<div id="param-test">
测试参数:<input id="url-input" type="text" placeholder="输入元数据参数 {imagerySet}|{mapLayer} 或 在线瓦片 url , 并回车" onkeydown="loadTile(this.value)" />
</div>
<div id="map" />
</body>
<script type="text/javascript">
L.TileLayer.BingTileLayer = L.TileLayer.extend({ // 拓展必应地图瓦片图层
initialize: function (options) {
function getTileUrl (coords) { // 提供的根据 xyz 计算瓦片 url 的方法,输入位置索引 {x: Number, y: Number, z: Number } ,输出对应的 url切片地址
var code = '' //必应地图切片规则,参考 https://learn.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system
for (var i = coords.z; i > 0; i--) {
var b = 0, mask = 1 << (i - 1)
if ((coords.x & mask) !== 0) { b += 1 }
if ((coords.y & mask) !== 0) { b += 2 }
code += b.toString()
}
return L.Util.template(this._url, L.extend({ subdomain: this._getSubdomain(coords), quadkey: code, culture: "zh-Hans" }))
}
if (options.url) { // 从在线瓦片 url 请求
options = L.extend({ subdomains: ["t0", "t1", "t2", "t3"], minZoom: 0, maxZoom: 23, minNativeZoom: 3, maxNativeZoom: 18 }, options, { getTileUrl: getTileUrl }) // getTileUrl 放最后不被覆盖
L.TileLayer.prototype.initialize.call(this, options.url, options)
}
else { // 从元数据 url 请求
L.Util.setOptions(this, options) // 当异步获取图层 url 时,可能会先执行图层的其他方法,先 setOptions 一下能够让其他方法获取到 this.options
options.metaDataUrl = L.Util.template("https://dev.ditu.live.com/REST/V1/Imagery/Metadata/{imagerySet}?{mapLayer}include=ImageryProviders&suppressStatus=true&uriScheme=https&o=json&ur=cn&c=zh-cn&key={BingMapsKey}",
{ imagerySet: options.imagerySet, mapLayer: options.mapLayer === "" ? "" : "mapLayer=" + options.mapLayer + "&", BingMapsKey: "AmLYMlFcLiYKOL_0K304vMOqq18dTfeBlja7GoB_-2KJltJXDmIBU7vaHG-xyEm2" })
this._metaDataFetch = fetch(options.metaDataUrl).then(res => res.json()).then((metaData) => {
if (metaData.statusCode === 200) {
var mata = metaData.resourceSets[0].resources[0]
options.url = mata.imageUrl
options = L.extend({ subdomains: mata.imageUrlSubdomains, minZoom: 0, maxZoom: 23, minNativeZoom: mata.zoomMin < 3 ? 3 : mata.zoomMin, maxNativeZoom: mata.zoomMax > 18 ? 18 : mata.zoomMax }, options, { getTileUrl: getTileUrl })
L.TileLayer.prototype.initialize.call(this, options.url, options)
console.dir(options.url)
} else {
console.dir("请求元数据出错: " + metaData.statusCode + ":" + metaData.statusDescription)
}
})
}
},
createTile: function (coords, done) {
const tile = document.createElement('img')
L.DomEvent.on(tile, 'load', this._tileOnLoad.bind(this, done, tile))
L.DomEvent.on(tile, 'error', this._tileOnError.bind(this, done, tile))
if (this.options.crossOrigin || this.options.crossOrigin === '') { tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin }
if (typeof this.options.referrerPolicy === 'string') { tile.referrerPolicy = this.options.referrerPolicy }
tile.alt = ''
if (this._metaDataFetch) { this._metaDataFetch.then(function () { tile.src = this.getTileUrl(coords) }.bind(this)) } // 从元数据异步请求
else { tile.src = this.getTileUrl(coords) }
return tile
},
getTileUrl: function (coords) {
if (this.options.getTileUrl) { // 存在自定义获取瓦片 url 方法
return this.options.getTileUrl.bind(this)(coords)
} else {
return L.TileLayer.prototype.getTileUrl.call(this, coords)
}
},
_setZoomTransform: function (level, center, zoom) {
if (this.options.getCorrect) { center = this.options.getCorrect(center) } // 存在纠偏方法,则进行纠偏计算
L.TileLayer.prototype._setZoomTransform.call(this, level, center, zoom)
},
_getTiledPixelBounds: function (center) {
if (this.options.getCorrect) { center = this.options.getCorrect(center) } // 存在纠偏方法,则进行纠偏计算
return L.TileLayer.prototype._getTiledPixelBounds.call(this, center)
}
})
L.tileLayer.bingTileLayer = function (options) { return new L.TileLayer.BingTileLayer(options) }
var map = L.map("map", { center: [29.708050, 118.321499], zoom: 15, zoomControl: false, attributionControl: false, doubleClickZoom: false })
L.marker([29.708050, 118.321499]).addTo(map) // 添加点用于纠偏测试
// map.on('dblclick', function (e) { console.dir(e.latlng.lng + "," + e.latlng.lat) })
var test_Layer = null
function loadTile (param) {
if (!window.event || window.event.keyCode === 13) { // keyCode ===13 表示按下回车
if (test_Layer !== null) { map.removeLayer(test_Layer), test_Layer = null }
if (!param) return
// bingTileLayer 在 tileLayer 的基础上增加了四个参数,从在线瓦片 url 请求 设置 url,从元数据请求设置 imagerySet 和 mapLayer;需要纠偏设置 getCorrect
if (param.split("|").length > 1) {
test_Layer = L.tileLayer.bingTileLayer({
imagerySet: param.split("|")[0], mapLayer: param.split("|")[1], getCorrect: (o) => { // 坐标系纠偏方法,输入输出都是 L.latLng 对象
return L.latLng(gcoord.transform([o.lng, o.lat], gcoord.WGS84, gcoord.GCJ02).reverse())
}
})
}
else {
test_Layer = L.tileLayer.bingTileLayer({
url: param, getCorrect: (o) => { // 坐标系纠偏方法,输入输出都是 L.latLng 对象
return L.latLng(gcoord.transform([o.lng, o.lat], gcoord.WGS84, gcoord.GCJ02).reverse())
}
})
}
map.addLayer(test_Layer)
}
}
var param = "Aerial|"
document.getElementById("url-input").value = param
loadTile(param)
</script>
</html>
参考链接
微软必应地图官网教程:https://learn.microsoft.com/en-us/bingmaps/rest-services/