类名 document/support/mapbox/leafletMapbox.js
import * as T from '@turf/turf'
/**
 * @description: leaflet内使用mapboxgl插件,copy from https://github.com/mapbox/mapbox-gl-leaflet
 * 初始版本:0.0.16
 * 改写后支持接入zondy mapboxgl内核库
 * @param {*} L
 * @param {*} mapboxgl
 * @return {*}
 */
export default function leafletMapbox(L, mapboxgl) {
  L.MapboxGL = L.Layer.extend({
    options: {
      updateInterval: 32,
      // How much to extend the overlay view (relative to map size)
      // e.g. 0.1 would be 10% of map view in each direction
      // 内边距设置为1,在旋转时可以遮住当前屏幕范围
      padding: 1,
      // whether or not to register the mouse and keyboard
      // events on the mapbox overlay
      interactive: false,
      // set the tilepane as the default pane to draw gl tiles
      pane: 'tilePane',
      // 裁剪区域
      clippingArea: null,
      // 级数偏移
      zoomOffset: 0
    },

    initialize(options) {
      L.setOptions(this, options)

      if (options.accessToken) {
        mapboxgl.accessToken = options.accessToken
      }

      // setup throttling the update event when panning
      this._throttledUpdate = L.Util.throttle(
        this._update,
        this.options.updateInterval,
        this
      )
    },

    onAdd(map) {
      if (!this._container) {
        this._initContainer()
      }

      const paneName = this.getPaneName()
      map.getPane(paneName).appendChild(this._container)

      this._initGL()

      this._offset = this._calcRotateOffset()

      // work around https://github.com/mapbox/mapbox-gl-leaflet/issues/47
      if (map.options.zoomAnimation) {
        L.DomEvent.on(
          map._proxy,
          L.DomUtil.TRANSITION_END,
          this._transitionEnd,
          this
        )
      }

      map._addZoomLimit(this)
    },

    onRemove(map) {
      if (this._map._proxy && this._map.options.zoomAnimation) {
        L.DomEvent.off(
          this._map._proxy,
          L.DomUtil.TRANSITION_END,
          this._transitionEnd,
          this
        )
      }
      const paneName = this.getPaneName()
      map.getPane(paneName).removeChild(this._container)
    },

    getEvents() {
      return {
        move: this._throttledUpdate, // sensibly throttle updating while panning
        zoomanim: this._animateZoom, // applys the zoom animation to the <canvas>
        zoom: this._pinchZoom, // animate every zoom event for smoother pinch-zooming
        zoomstart: this._zoomStart, // flag starting a zoom to disable panning
        zoomend: this._zoomEnd,
        resize: this._resize
      }
    },
    setClippingArea(data) {
      this.options.clippingArea = data
      this.clippingAreaBounds = null
      this._update()
    },
    getClippingArea() {
      return this.options.clippingArea
    },
    getMapboxMap() {
      return this._glMap
    },

    getCanvas() {
      return this._glMap.getCanvas()
    },

    getSize() {
      return this._map.getSize().multiplyBy(1 + this.options.padding * 2)
    },

    getBounds() {
      const halfSize = this.getSize().multiplyBy(0.5)
      const center = this._map.latLngToContainerPoint(this._map.getCenter())
      return L.latLngBounds(
        this._map.containerPointToLatLng(center.subtract(halfSize)),
        this._map.containerPointToLatLng(center.add(halfSize))
      )
    },

    getContainer() {
      return this._container
    },

    // returns the pane name set in options if it is a valid pane, defaults to tilePane
    getPaneName() {
      return this._map.getPane(this.options.pane)
        ? this.options.pane
        : 'tilePane'
    },

    _initContainer() {
      const container = (this._container = L.DomUtil.create(
        'div',
        'leaflet-gl-layer'
      ))

      const size = this.getSize()

      container.style.width = `${size.x}px`
      container.style.height = `${size.y}px`

      const topLeft = this._calcRotateOffset()

      L.DomUtil.setPosition(container, topLeft)
    },

    _initGL() {
      const center = this._map.getCenter()
      const crs = this._map.options.crs
      this._offsetZoom = 0
      let projCRS
      const crsOptions = crs.options

      if (crsOptions) {
        const mapboxTileSize = crsOptions.mapboxTileSize || 256
        const projCRSOptions = {
          resolutions: crsOptions.resolutions,
          origin: crsOptions.origin,
          tileSize: mapboxTileSize
        }
        if (mapboxgl.Proj && mapboxgl.Proj.CRS) {
          projCRS = new mapboxgl.Proj.CRS(
            crs.code,
            crs.def || crs.code,
            projCRSOptions
          )
          // projCRS getZoomOffset计算投影坐标系的偏移(针对超出投影系范围进行偏移处理)
          // 计算当前使用的size,因为本质上是mapbox在渲染,并没有依赖于leaflet的网格图层,我们需要知道对应leaflet层级的瓦片是否正确,如果瓦片大小是512,需要偏移下mapbox的zoom层级
          // Math.floor(mapboxTileSize/256 -1 )
          this._offsetZoom =
            projCRS.getZoomOffset() -
            Math.floor(mapboxTileSize / 256 - 1) -
            this.options.zoomOffset
        } else {
          if (crsOptions.bounds) {
            const max = crsOptions.bounds.max
            const min = crsOptions.bounds.min
            projCRSOptions.bounds = [min.x, min.y, max.x, max.y]
          }
          projCRS = new mapboxgl.CRS(crs.code, projCRSOptions)
        }
      } else {
        projCRS = crs.code
      }

      const options = L.extend({}, this.options, {
        container: this._container,
        center: [center.lng, center.lat],
        zoom: this._map.getZoom() - 1,
        attributionControl: false,
        crs: projCRS,
        minZoom: 0,
        renderWorldCopies: false
      })

      if (!this._glMap) this._glMap = new mapboxgl.Map(options)
      else {
        this._glMap.setCenter(options.center)
        this._glMap.setZoom(options.zoom)
      }
      // allow GL base map to pan beyond min/max latitudes
      this._glMap.transform.latRange = null
      this._transformGL(this._glMap)

      if (this._glMap._canvas.canvas) {
        // older versions of mapbox-gl surfaced the canvas differently
        this._glMap._actualCanvas = this._glMap._canvas.canvas
      } else {
        this._glMap._actualCanvas = this._glMap._canvas
      }

      // treat child <canvas> element like L.ImageOverlay
      const canvas = this._glMap._actualCanvas
      L.DomUtil.addClass(canvas, 'leaflet-image-layer')
      L.DomUtil.addClass(canvas, 'leaflet-zoom-animated')
      if (this.options.interactive) {
        L.DomUtil.addClass(canvas, 'leaflet-interactive')
      }
      if (this.options.className) {
        L.DomUtil.addClass(canvas, this.options.className)
      }
    },
    /**
     * @description: 计算旋转实际的偏移值
     * @return {*}
     */
    _calcRotateOffset() {
      // 需要注意的是,mapboxgl容器实际上是以中心点作为和leaflet覆盖层关联的点
      // 也就是说,mapboxgl容器和leaflet容器中心点是一致的
      // 旋转时如果还是根据左上角点计算,则在旋转后会出现偏移,因此以中心点作为偏移的基准点
      const offset = this._map.getSize().multiplyBy(this.options.padding)
      const viewHalf = this._map.getSize()._divideBy(2)
      const topLeft = this._map
        ._latLngToNewLayerPoint(
          this._map.getCenter(),
          this._map.getZoom(),
          this._map.getCenter()
        )
        .subtract(viewHalf)
        .subtract(offset)
      return topLeft
    },
    _update(e) {
      if (!this._map) {
        return
      }
      // update the offset so we can correct for it later when we zoom
      this._offset = this._calcRotateOffset()

      if (this._zooming) {
        return
      }

      const gl = this._glMap
      const size = this.getSize()

      const container = this._container

      this._transformGL(gl)

      const x_round = Math.round(size.x)
      const y_round = Math.round(size.y)

      if (
        Math.round(gl.transform.width) !== x_round ||
        Math.round(gl.transform.height) !== y_round
      ) {
        container.style.width = `${x_round}px`
        container.style.height = `${y_round}px`

        gl.transform.width = x_round
        gl.transform.height = y_round

        if (gl._resize !== null && gl._resize !== undefined) {
          gl._resize()
        } else {
          gl.resize()
        }
      } else {
        // older versions of mapbox-gl surfaced update publicly
        if (gl._update !== null && gl._update !== undefined) {
          gl._update()
        } else {
          gl.update()
        }
      }
    },

    _transformGL(gl) {
      const center = this._clampCenter(this._map.getCenter())
      // gl.setView([center.lat, center.lng], this._map.getZoom() - 1, 0);
      // calling setView directly causes sync issues because it uses requestAnimFrame
      const tr = gl.transform
      tr.center = mapboxgl.LngLat.convert([center.lng, center.lat])
      // 设置不进行自动重算zoom center
      tr.resetZoomScale = false

      // transfrom zoom等级为整数,否则会出现瓦片和实际位置偏移
      // offsetZoom定义为mapboxgl内部层级偏移值,非0情况下表示,由于此投影坐标系超出投影系范围,故进行偏移
      tr.zoom = Math.floor(this._map.getZoom() - this._offsetZoom - 1)
      const currentCenter = tr.center
      const centerOffset = [0, 0]
      if (
        Math.abs(currentCenter.lng - center.lng) > 10e-8 ||
        Math.abs(currentCenter.lat - center.lat) > 10e-8
      ) {
        const p1 = tr.coordinatePoint(tr.locationCoordinate(currentCenter))
        const p2 = tr.coordinatePoint(tr.locationCoordinate(center))
        centerOffset[0] = p1.x - p2.x
        centerOffset[1] = p1.y - p2.y
      }

      // 偏移container 需要对照mapboxgl内核库偏移
      const container = this._container
      const topLeft = this._calcRotateOffset().add(centerOffset)
      // 矢量瓦片裁剪处理程序
      const polygon = this.options.clippingArea
      if (polygon) {
        // 计算裁剪区缓存四至
        if (!this.clippingAreaBounds) {
          const [x1, y1, x2, y2] = T.bbox(T.polygon(polygon))
          this.clippingAreaBounds = L.latLngBounds(
            L.latLng(y1, x1),
            L.latLng(y2, x2)
          )
        }
        // 先进行四至边界判断
        if (this.clippingAreaBounds.intersects(this._map.getBounds())) {
          const clipPxs = polygon.map((coords) => {
            return coords.map((v) => {
              const point = this._map.latLngToContainerPoint(
                L.latLng(v[1], v[0])
              )
              return [point.x, point.y]
            })
          })
          const polygon1 = T.polygon(clipPxs)
          const size = this._map.getSize()
          const polygon2 = T.polygon([
            [
              [0, 0],
              [size.x, 0],
              [size.x, size.y],
              [0, size.y],
              [0, 0]
            ]
          ])

          const intersection = T.intersect(polygon1, polygon2)
          if (intersection) {
            const coords = T.coordAll(intersection)
            if (coords && coords.length > 3) {
              let clipStr = ''
              coords.forEach((coord) => {
                clipStr += `${coord[0]}px ${coord[1]}px,`
              })
              clipStr = clipStr.substring(0, clipStr.length - 1)
              container.style.clipPath = `polygon(${clipStr})`
            }
          }
        } else {
          container.style.clipPath = 'none'
        }
      } else {
        container.style.clipPath = 'none'
      }

      L.DomUtil.setPosition(container, topLeft)
    },
    _clampCenter(center) {
      if (!center) return center
      const lng = center.lng
      const lat = Math.min(Math.max(center.lat, -90), 90)
      return L.latLng(lat, lng)
    },
    // update the map constantly during a pinch zoom
    _pinchZoom(e) {
      this._glMap.jumpTo({
        zoom: Math.floor(this._map.getZoom() - this._offsetZoom - 1),
        center: this._clampCenter(this._map.getCenter())
      })
    },

    // borrowed from L.ImageOverlay
    // https://github.com/Leaflet/Leaflet/blob/master/src/layer/ImageOverlay.js#L139-L144
    _animateZoom(e) {
      const scale = this._map.getZoomScale(e.zoom)
      const padding = this._map
        .getSize()
        .multiplyBy(this.options.padding * scale)
      const viewHalf = this.getSize()._divideBy(2)
      // corrections for padding (scaled), adapted from
      // https://github.com/Leaflet/Leaflet/blob/master/src/map/Map.js#L1490-L1508
      const topLeft = this._map
        .project(e.center, e.zoom)
        ._subtract(viewHalf)
        ._add(this._map._getMapPanePos().add(padding))
        ._round()
      const offset = this._map
        .project(this._map.getBounds().getNorthWest(), e.zoom)
        ._subtract(topLeft)

      L.DomUtil.setTransform(
        this._glMap._actualCanvas,
        offset.subtract(this._offset),
        scale
      )
    },

    _zoomStart(e) {
      this._zooming = true
      // 处理动画效果异常问题
      if (this._container) {
        this._container.style.visibility = 'hidden'
      }
      this._offset = this._calcRotateOffset()
    },

    _zoomEnd() {
      const scale = this._map.getZoomScale(this._map.getZoom())

      L.DomUtil.setTransform(
        this._glMap._actualCanvas,
        // https://github.com/mapbox/mapbox-gl-leaflet/pull/130
        null,
        scale
      )

      this._zooming = false
      this._update()
      // 处理动画效果异常问题
      if (this._container) {
        this._container.style.visibility = 'visible'
      }
    },

    _transitionEnd(e) {
      L.Util.requestAnimFrame(function () {
        const zoom = this._map.getZoom()
        const center = this._map.getCenter()
        const offset = this._map.latLngToContainerPoint(
          this._map.getBounds().getNorthWest()
        )

        // reset the scale and offset
        L.DomUtil.setTransform(this._glMap._actualCanvas, offset, 1)

        // enable panning once the gl map is ready again
        this._glMap.once(
          'moveend',
          L.Util.bind(function () {
            this._zoomEnd()
          }, this)
        )

        // update the map position
        this._glMap.jumpTo({
          center: this._clampCenter(center),
          zoom: Math.floor(zoom - this._offsetZoom - 1)
        })
      }, this)
    },

    _resize(e) {
      this._transitionEnd(e)
    }
  })

  L.mapboxGL = function (options) {
    return new L.MapboxGL(options)
  }
}
构造函数
成员变量
方法
事件