截至当前,在 Leaflet 中,地图本身和其内所有图层使用一个同坐标系,在创建 Map 时默认指定 { crs: L.CRS.EPSG3857 }。
通过百度地图和天地图的加载方法我们我们知道,天地图使用 WGS84 通用坐标系(代码中体现为 Leaflet 默认指定的 L.CRS.EPSG3857 坐标参考系,事实上除了百度地图,其他在线瓦片地图都可以使用此坐标参考系),百度地图使用 BD09 百度坐标系(代码中体现为自定义的 L.CRS.Baidu 坐标参考系)。
本文为了同地图叠加两种地图,拓展了 GridLayer 和 TileLayer,添加了图层的通用坐标系配置,将 GridLayer 和 TileLayer 源码中涉及到默认使用地图通用坐标系的地方换成了图层配置的通用坐标系。
仅在初始化的时候能够正确叠加,缩放和平移之后会出现问题!!!!!!!!!!
相关代码
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<style type="text/css">
body { padding: 0; margin: 0; }
html, body, #map { height: 100%; }
</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>
<script type="text/javascript">
L.Projection.BaiduMercator = L.Util.extend({}, L.Projection.Mercator, {
R: 6378206, //百度椭球赤道半径 a=6378206,相当于在 WGS84 椭球赤道半径上加了 69 米
R_MINOR: 6356584.314245179, //百度椭球极半径 b=6356584.314245179,相当于在 WGS84 椭球极半径上减了 168 米
bounds: new L.Bounds([-20037725.11268234, -19994619.55417086], [20037725.11268234, 19994619.55417086]) //数据覆盖范围在经度[-180°,180°],纬度[-85.051129°, 85.051129°]之间
})
L.CRS.Baidu = L.Util.extend({}, L.CRS.Earth, {
code: 'EPSG:Baidu',
projection: L.Projection.BaiduMercator,
transformation: new L.transformation(1, 0.5, -1, 0.5),
scale: function (zoom) { return 1 / Math.pow(2, (18 - zoom)) },
zoom: function (scale) { return 18 - Math.log(1 / scale) / Math.LN2 }
})
L.CustomGridLayer = L.GridLayer.extend({ //自定义网格层,在所有使用地图坐标系的地方 换上网格层自定义的坐标系,与源码不一致的地方都用 this.options.crs === undefined 做了三元运算符判断或加了注释 // custom
_updateLevels: function () {
const zoom = this._tileZoom, maxZoom = this.options.maxZoom;
if (zoom === undefined) { return undefined; }
for (let z in this._levels) {
z = Number(z);
if (this._levels[z].el.children.length || z === zoom) {
this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
this._onUpdateLevel(z);
} else {
L.DomUtil.remove(this._levels[z].el);
this._removeTilesAtZoom(z);
this._onRemoveLevel(z);
delete this._levels[z];
}
}
let level = this._levels[zoom];
const map = this._map;
if (!level) {
level = this._levels[zoom] = {};
level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
level.el.style.zIndex = maxZoom;
level.origin = this.options.crs === undefined ?
(map.project(map.unproject(map.getPixelOrigin()), zoom).round()) :
(this.project(this.unproject(this.getPixelOrigin()), zoom).round()); // custom
level.zoom = zoom;
this.options.crs === undefined ?
(this._setZoomTransform(level, map.getCenter(), map.getZoom())) :
(this._setZoomTransform(level, this.getCenter(), map.getZoom())); // custom
L.Util.falseFn(level.el.offsetWidth);
this._onCreateLevel(level);
}
this._level = level;
return level;
},
_resetView: function (e) {
const animating = e && (e.pinch || e.flyTo);
this.options.crs === undefined ?
(this._setView(this._map.getCenter(), this._map.getZoom(), animating, animating)) :
(this._setView(this.getCenter(), this._map.getZoom(), animating, animating)); // custom
},
_animateZoom: function (e) {
this.options.crs === undefined ?
(this._setView(e.center, e.zoom, true, e.noUpdate)) :
(this._setView(this.convertCenter(e.center), e.zoom, true, e.noUpdate)); // custom
},
_setZoomTransform: function (level, center, zoom) {
const scale = this.options.crs === undefined ?
(this._map.getZoomScale(zoom, level.zoom)) :
(this.getZoomScale(zoom, level.zoom)), // custom
translate = this.options.crs === undefined ?
(level.origin.multiplyBy(scale).subtract(this._map._getNewPixelOrigin(center, zoom)).round()) :
(level.origin.multiplyBy(scale).subtract(this._getNewPixelOrigin(center, zoom)).round()); // custom
if (L.Browser.any3d) {
L.DomUtil.setTransform(level.el, translate, scale);
} else {
L.DomUtil.setPosition(level.el, translate);
}
},
_resetGrid: function () {
const crs = this.options.crs === undefined ? this._map.options.crs : this.options.crs, // custom
tileSize = this._tileSize = this.getTileSize(),
tileZoom = this._tileZoom;
const bounds = this.options.crs === undefined ? this._map.getPixelWorldBounds(this._tileZoom) : this.getPixelWorldBounds(this._tileZoom); // custom
if (bounds) { this._globalTileRange = this._pxBoundsToTileRange(bounds); }
this._wrapX = this.options.crs === undefined ?
(crs.wrapLng && !this.options.noWrap && [Math.floor(map.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x), Math.ceil(map.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)]) :
(crs.wrapLng && !this.options.noWrap && [Math.floor(this.project([0, crs.wrapLng[0]], tileZoom).x / tileSize.x), Math.ceil(this.project([0, crs.wrapLng[1]], tileZoom).x / tileSize.y)]); // custom
this._wrapY = this.options.crs === undefined ?
(crs.wrapLat && !this.options.noWrap && [Math.floor(map.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x), Math.ceil(map.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)]) :
(crs.wrapLat && !this.options.noWrap && [Math.floor(this.project([crs.wrapLat[0], 0], tileZoom).y / tileSize.x), Math.ceil(this.project([crs.wrapLat[1], 0], tileZoom).y / tileSize.y)]); // custom
},
_getTiledPixelBounds: function (center) {
const map = this._map,
mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
scale = this.options.crs === undefined ? map.getZoomScale(mapZoom, this._tileZoom) : this.getZoomScale(mapZoom, this._tileZoom), // custom
pixelCenter = this.options.crs === undefined ? map.project(center, this._tileZoom).floor() : this.project(center, this._tileZoom).floor(), // custom
halfSize = map.getSize().divideBy(scale * 2);
return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
},
_update: function (center) {
if (center === undefined) { center = this.options.crs === undefined ? map.getCenter() : this.getCenter(); } // custom
L.GridLayer.prototype._update.call(this, center)
},
_isValidTile: function (coords) {
const crs = this.options.crs === undefined ? this._map.options.crs : this.options.crs; // custom
if (!crs.infinite) {
const bounds = this._globalTileRange;
if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
(!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
}
if (!this.options.bounds) { return true; }
const tileBounds = this._tileCoordsToBounds(coords);
return L.latLngBounds(this.options.bounds).overlaps(tileBounds);
},
_tileCoordsToNwSe: function (coords) {
const tileSize = this.getTileSize(),
nwPoint = coords.scaleBy(tileSize),
sePoint = nwPoint.add(tileSize),
nw = this.options.crs === undefined ? this._map.unproject(nwPoint, coords.z) : this.unproject(nwPoint, coords.z), // custom
se = this.options.crs === undefined ? this._map.unproject(sePoint, coords.z) : this.unproject(sePoint, coords.z); // custom
return [nw, se];
},
_tileCoordsToBounds: function (coords) {
const bp = this._tileCoordsToNwSe(coords);
let bounds = new L.LatLngBounds(bp[0], bp[1]);
if (!this.options.noWrap) { bounds = this.options.crs === undefined ? this._map.wrapLatLngBounds(bounds) : this.wrapLatLngBounds(bounds); } // custom
return bounds;
},
getCenter () { // custom
return this.convertCenter(this._map.getCenter())
},
convertCenter (center) { // custom
const point = this._map.latLngToLayerPoint(center)
const projectedPoint = L.point(point).add(this.getPixelOrigin());
return this.unproject(projectedPoint);
},
getPixelOrigin () { // custom
this._map._checkIfLoaded();
return this._getNewPixelOrigin(this._map._lastCenter);
},
_getNewPixelOrigin (center, zoom) { // custom
const viewHalf = this._map.getSize()._divideBy(2);
return this.project(center, zoom)._subtract(viewHalf)._add(this._map._getMapPanePos())._round();
},
getPixelWorldBounds (zoom) { // custom
return this.options.crs.getProjectedBounds(zoom === undefined ? this._map.getZoom() : zoom);
},
getZoomScale (toZoom, fromZoom) { // custom
const crs = this.options.crs;
fromZoom = fromZoom === undefined ? this._map._zoom : fromZoom;
return crs.scale(toZoom) / crs.scale(fromZoom);
},
project (latlng, zoom) { // custom
zoom = zoom === undefined ? this._map._zoom : zoom;
return this.options.crs.latLngToPoint(L.latLng(latlng), zoom);
},
unproject (point, zoom) { // custom
zoom = zoom === undefined ? this._map._zoom : zoom;
return this.options.crs.pointToLatLng(L.point(point), zoom);
},
wrapLatLngBounds (latlng) { // custom
return this.options.crs.wrapLatLngBounds(L.latLngBounds(latlng));
},
})
L.CustomTileLayer = L.CustomGridLayer.extend({ //自定义瓦片层,基于自定义网格层拓展,很多方法直接劫持调用了 L.TileLayer 的对应方法
options: {
minZoom: 0,
maxZoom: 18,
subdomains: 'abc',
errorTileUrl: '',
zoomOffset: 0, tms: false,
zoomReverse: false,
detectRetina: false,
crossOrigin: false,
referrerPolicy: false
},
initialize (url, options) { L.TileLayer.prototype.initialize.call(this, url, options); },
setUrl (url, noRedraw) { return L.TileLayer.prototype.setUrl.call(this, url, noRedraw); },
createTile (coords, done) { return L.TileLayer.prototype.createTile.call(this, coords, done); },
getTileUrl (coords) {
const data = { r: L.Browser.retina ? '@2x' : '', s: this._getSubdomain(coords), x: coords.x, y: coords.y, z: this._getZoomForUrl() };
if (this.options.crs === undefined ? (this._map && !this._map.options.crs.infinite) : (!this.options.crs.infinite)) { // custom
const invertedY = this._globalTileRange.max.y - coords.y;
if (this.options.tms) { data['y'] = invertedY; }
data['-y'] = invertedY;
}
return L.Util.template(this._url, L.Util.extend(data, this.options));
},
_tileOnLoad (done, tile) { done(null, tile); },
_tileOnError (done, tile, e) { L.TileLayer.prototype._tileOnError.call(this, done, tile, e); },
_onTileRemove (e) { e.tile.onload = null; },
_getZoomForUrl () { return L.TileLayer.prototype._getZoomForUrl.call(this); },
_getSubdomain (tilePoint) { return L.TileLayer.prototype._getSubdomain.call(this, tilePoint); },
_abortLoading () { L.TileLayer.prototype._abortLoading.call(this); },
_removeTile (key) { return L.TileLayer.prototype._removeTile.call(this, key); },
_tileReady (coords, err, tile) { return L.TileLayer.prototype._tileReady.call(this, coords, err, tile); }
})
</script>
</head>
<body>
<div id="map" />
</body>
<script type="text/javascript">
L.TileLayer.TdtTileLayer = L.TileLayer.extend({ // 拓展天地图瓦片图层
initialize: function (type, key, options) {
var templateUrl = "//t{s}.tianditu.gov.cn/DataServer?T={t}&x={x}&y={y}&l={z}&tk={k}"
options = L.extend({ t: type, l: type.substr(0, 3), k: key, subdomains: "01234567", minZoom: 0, maxZoom: 23, minNativeZoom: type === "ibo_w" ? 3 : 1, maxNativeZoom: type === "ibo_w" ? 10 : 18 }, options)
L.TileLayer.prototype.initialize.call(this, templateUrl, options)
}
})
L.tileLayer.tdtTileLayer = function (type, key, options) { return new L.TileLayer.TdtTileLayer(type, key, options) }
L.TileLayer.BaiDuTileLayer = L.CustomTileLayer.extend({
initialize: function (param, options) {
var templateImgUrl = "//maponline{s}.bdimg.com/starpic/u=x={x};y={y};z={z};v=009;type=sate&qt=satepc&fm=46&app=webearth2&v=009"
var templateUrl = "//maponline{s}.bdimg.com/tile/?x={x}&y={y}&z={z}&{p}"
var myUrl = (param === "img" ? templateImgUrl : templateUrl)
options = L.extend({
getUrlArgs: (o) => {
return { x: o.x, y: (-1 - o.y), z: o.z }
// return { x: o.x - (1 << (o.z - 1)), y: (1 << (o.z - 1)) - o.y - 1, z: o.z } // 右上角往右往下 转 中心点往右往上
},
p: param, subdomains: "0123", minZoom: 0, maxZoom: 23, minNativeZoom: 1, maxNativeZoom: 18, crs: L.CRS.Baidu
}, options)
L.CustomTileLayer.prototype.initialize.call(this, myUrl, options)
},
getTileUrl: function (coords) {
if (this.options.getUrlArgs) { return L.Util.template(this._url, L.extend({ s: this._getSubdomain(coords), r: L.Browser.retina ? '@2x' : '' }, this.options.getUrlArgs(coords), this.options)) }
else { return L.CustomTileLayer.prototype.getTileUrl.call(this, coords) }
},
_setZoomTransform: function (level, center, zoom) {
center = L.latLng(gcoord.transform([center.lng, center.lat], gcoord.WGS84, gcoord.BD09).reverse()) // 采用 gcoord 库进行纠偏
L.CustomTileLayer.prototype._setZoomTransform.call(this, level, center, zoom)
},
_getTiledPixelBounds: function (center) {
center = L.latLng(gcoord.transform([center.lng, center.lat], gcoord.WGS84, gcoord.BD09).reverse()) // 采用 gcoord 库进行纠偏
return L.CustomTileLayer.prototype._getTiledPixelBounds.call(this, center)
}
})
L.tileLayer.baiDuTileLayer = function (param, options) { return new L.TileLayer.BaiDuTileLayer(param, options) }
var bdimg_Layer = L.tileLayer.baiDuTileLayer("img") // 百度影像底图
var tdtimg_Layer = L.tileLayer.tdtTileLayer("img_w", "d083e4cf30bfc438ef93436c10c2c20a") // 天地图影像
var map = L.map("map", { center: [29.708050, 118.321499], zoom: 17, zoomControl: false, attributionControl: false, doubleClickZoom: false })
var overlayLayers = { "百度影像底图": bdimg_Layer, "天地图影像": tdtimg_Layer }
L.control.layers([], overlayLayers, { autoZIndex: false }).addTo(map)
bdimg_Layer.addTo(map)
L.marker([29.708050, 118.321499]).addTo(map)
</script>
</html>