关于在 Leaflet 中同地图联动叠加百度地图、天地图在线瓦片的一点探索

截至当前,在 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>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值