类名 util/extend/layer/tile/L.TileLayer.Clip.js
import * as L from '@mapgis/leaflet'
// eslint-disable-next-line import/no-extraneous-dependencies
import * as T from '@turf/turf'
import { defaultValue, FetchMethod } from '@mapgis/webclient-common'

L.TileLayer.Clip = L.TileLayer.extend({
  options: {
    clippingArea: null
  },
  initialize(url, options) {
    L.TileLayer.prototype.initialize.call(this, url, options)
    // 缓存clippingArea像素边界
    this._clippingAreaPx = null
    // 缓存瓦片裁剪区
    this._TileClippingGeometryCache = {}
    // 设置请求方式
    this.httpMethod = defaultValue(options.httpMethod, FetchMethod.get)
  },
  createTile(coords, done) {
    const src = this.getTileUrl(coords)
    return this._drawClippingTile(src, coords, done)
  },
  /**
   * @description: 设置裁剪区域
   * @param {*} data
   * @return {*}
   */
  setClippingArea(data) {
    this.options.clippingArea = data
    this._clearClippingCache()
    this.redraw()
    return this
  },
  /**
   * @description: 获取裁剪区域
   * @return {*}
   */
  getClippingArea() {
    return this.options.clippingArea
  },
  /**
   * @description: 根据瓦片行列号获取空间裁剪区
   * @param {*} x
   * @param {*} y
   * @param {*} z
   * @return {*}
   */
  _getTileGeometryAndState(x, y, z) {
    // 不设置裁剪区默认不裁剪
    if (!this.options.clippingArea) {
      return {
        in: true,
        geometry: null
      }
    }
    // 缓存瓦片id
    const cacheId = `${x},${y},${z}`
    // 返回缓存的几何
    if (this._TileClippingGeometryCache[cacheId]) {
      return {
        intersect: true,
        geometry: this._TileClippingGeometryCache[cacheId]
      }
    }

    const clippingAreaPx = this._getClippingAreaPx()
    const tileSize = this.options.tileSize
    const zLevel = Math.pow(2, z - this.options.zoomOffset)
    // 计算0级范围
    const x1 = (x * tileSize) / zLevel
    const y1 = (y * tileSize) / zLevel
    const x2 = ((x + 1) * tileSize) / zLevel
    const y2 = ((y + 1) * tileSize) / zLevel
    const tileBbox = T.polygon([
      [
        [x1, y1],
        [x2, y1],
        [x2, y2],
        [x1, y2],
        [x1, y1]
      ]
    ])

    // 判断裁剪区>瓦片范围
    if (T.booleanContains(clippingAreaPx, tileBbox)) {
      return {
        in: true,
        geometry: null
      }
    }

    // 判断裁剪区<瓦片范围
    if (T.booleanContains(tileBbox, clippingAreaPx)) {
      this._TileClippingGeometryCache[cacheId] = clippingAreaPx
      return {
        intersect: true,
        geometry: clippingAreaPx
      }
    }

    // 判断是否相交
    if (!T.booleanOverlap(clippingAreaPx, tileBbox)) {
      return {
        out: true,
        geometry: null
      }
    }

    const intersectResult = T.intersect(clippingAreaPx, tileBbox)
    if (!intersectResult) {
      return {
        out: true,
        geometry: null
      }
    }

    this._TileClippingGeometryCache[cacheId] = intersectResult

    return {
      intersect: true,
      geometry: intersectResult
    }
  },
  /**
   * 获取合并后的点集
   * @param {Number[][][]} polygon 面
   * @return {Number[][][]} 计算像素坐标系点
   */
  _toMercGeometry(polygons) {
    // 缓存0级地图下几何的相对位置
    const rings = []
    for (let j = 0; j < polygons.length; j++) {
      const ring = []
      for (let k = 0; k < polygons[j].length; k++) {
        const p = this._map.project(
          L.latLng(polygons[j][k][1], polygons[j][k][0]),
          0
        )
        ring.push([p.x, p.y])
      }
      rings.push(ring)
    }
    return rings
  },
  /**
   * 计算裁剪区像素坐标几何以及更新其四至
   * @return {Object|null} 合并后的点集
   */
  _getClippingAreaPx() {
    if (this._clippingAreaPx) {
      return this._clippingAreaPx
    }
    if (this.options.clippingArea) {
      this._clippingAreaPx = T.polygon(
        this._toMercGeometry(this.options.clippingArea)
      )
      this._clippingAreaPxBounds = T.bbox(this._clippingAreaPx)
    } else {
      this._clippingAreaPx = null
      this._clippingAreaPxBounds = null
    }
    return this._clippingAreaPx
  },
  /**
   * @description: 清空缓存
   * @return {*}
   */
  _clearClippingCache() {
    this._clippingAreaPx = null
    this._clippingAreaPxBounds = null
    this._TileClippingGeometryCache = {}
  },

  /**
   * @description: 重绘图片
   * @param {*} url
   * @param {*} coords
   * @param {*} done
   * @return {*}
   */
  _drawClippingTile(url, coords, done) {
    const { x, y, z } = coords
    const state = this._getTileGeometryAndState(x, y, z)
    // 创建tile
    const tile = document.createElement('img')
    // 处理状态
    if (state.in) {
      return this._getTileImage(tile, url, done)
    }

    if (state.out) {
      const canvas = document.createElement('canvas')
      canvas.width = this.options.width
      canvas.height = this.options.height
      return this._getTileImage(tile, null, done)
    }

    if (state.intersect && state.geometry) {
      this._getTileImage(tile, url, done)
      // 创建绘制的canvas
      const canvas = document.createElement('canvas')
      canvas.width = this.options.tileSize
      canvas.height = this.options.tileSize
      const ctx = canvas.getContext('2d')
      const tileSize = this.options.tileSize
      const tileX = tileSize * x
      const tileY = tileSize * y
      const zLevel = Math.pow(2, z - this.options.zoomOffset)
      const geometryCoords = T.coordAll(state.geometry)
      // 图像加载完毕绘制canvas
      tile.onload = function () {
        canvas.complete = true
        // 定时器
        setTimeout(() => {
          const pattern = ctx.createPattern(tile, 'repeat')
          let drawX
          let drawY
          // 开始绘制
          ctx.beginPath()
          // 移动到开始点
          drawX = geometryCoords[0][0] * zLevel - tileX
          drawY = geometryCoords[0][1] * zLevel - tileY
          ctx.moveTo(drawX, drawY)
          for (let j = 1; j < geometryCoords.length; j++) {
            drawX = geometryCoords[j][0] * zLevel - tileX
            drawY = geometryCoords[j][1] * zLevel - tileY
            // 开始绘制
            ctx.lineTo(drawX, drawY)
          }
          // 设置裁剪
          ctx.clip()
          ctx.beginPath()
          ctx.rect(0, 0, canvas.width, canvas.height)
          ctx.fillStyle = pattern
          ctx.fill()
          done()
        }, 0)
      }

      if (this.options.crossOrigin) {
        tile.crossOrigin = ''
      }
      return canvas
    }
  },
  _getTileImage(tile, url, done) {
    if (!url) return tile
    L.DomEvent.on(tile, 'load', L.Util.bind(this._tileOnLoad, this, done, tile))
    L.DomEvent.on(
      tile,
      'error',
      L.Util.bind(this._tileOnError, this, done, tile)
    )
    if (this.options.crossOrigin || this.options.crossOrigin === '') {
      tile.crossOrigin =
        this.options.crossOrigin === true ? '' : this.options.crossOrigin
    }
    // for this new option we follow the documented behavior
    // more closely by only setting the property when string
    if (typeof this.options.referrerPolicy === 'string') {
      tile.referrerPolicy = this.options.referrerPolicy
    }
    // The alt attribute is set to the empty string,
    // allowing screen readers to ignore the decorative image tiles.
    // https://www.w3.org/WAI/tutorials/images/decorative/
    // https://www.w3.org/TR/html-aria/#el-img-empty-alt
    tile.alt = ''

    // 验证是否为base64字符串
    let imageUrl = url
    if (!/data:image\/jpg;base64/.test(url)) {
      imageUrl = this._getInterceptorsUrl(url)
      const config = this._map.options.config
      tile.onload = function () {
        L.InterceptorsUtils.serviceConfigResponseInterceptors(
          config,
          imageUrl,
          tile
        )
      }
    }

    if (this.httpMethod === FetchMethod.post) {
      this._postImage(tile, url)
    } else {
      tile.src = imageUrl
    }
    return tile
  },

  // 由子类实现
  _postImage(tile, options) {},

  _getInterceptorsUrl(url) {
    // 处理拦截器代码
    const config = this._map.options.config
    const tokenStr = `&${L.InterceptorsUtils.serviceConfigToken(config)}`
    let imageUrl = L.InterceptorsUtils.serviceConfigRequestInterceptors(
      config,
      url
    )
    if (tokenStr !== '&') {
      imageUrl += tokenStr
    }
    return imageUrl
  }
})

L.TileLayer.clip = function (url, options) {
  return new L.TileLayer.Clip(url, options)
}
构造函数
成员变量
方法
事件