类名 leaflet/view/MapView.js
import {
  Zondy,
  defaultValue,
  BaseView,
  ViewEventType,
  LayerType,
  Point,
  Extent,
  T,
  showLayerByScale,
  SpatialReference,
  Projection,
  MapEventType,
  Geometry,
  Config,
  cloneObject,
  TileInfoUtil,
  // Utils,
  Map,
  isNull,
  toJSON
} from '@mapgis/webclient-common'
import * as L from '@mapgis/leaflet'
import UI from './ui/UI'
import MapViewUtil from './utils/MapViewUtil'
import { getLayerView } from '../document/index'
import CRS from './utils/support/CRS'
import Popup from './utils/Popup'
import Screenshot from './utils/Screenshot'

/**
 * 二维场景视图(leaflet引擎),对地图引擎进行管理,如果要对地图图层进行管理请参考[Map]{@link Map},<br/>
 * 参考示例:
 * <a href='#MapView'>[初始化二维场景视图]</a>
 * <br>[ES5引入方式]:<br/>
 * Zondy.MapView() <br/>
 * [ES6引入方式]:<br/>
 * import { MapView } from '@mapgis/webclient-leaflet-plugin' <br/>
 * @class MapView
 * @extends Evented
 * @moduleEX ViewModule
 * @fires BaseView#地图视图加载完毕事件
 * @fires BaseView#鼠标点击事件
 * @fires BaseView#鼠标双击事件
 * @fires BaseView#鼠标按下事件
 * @fires BaseView#鼠标抬起事件
 * @fires BaseView#鼠标右键点击事件
 * @fires BaseView#鼠标移动事件
 * @fires BaseView#鼠标移出视图事件
 * @fires BaseView#鼠标移入视图事件
 * @fires BaseView#zoom变化事件
 * @fires BaseView#地图移动事件
 * @fires BaseView#地图大小变化事件
 * @fires BaseView#键盘输入事件
 * @fires BaseView#键盘按下事件
 * @fires BaseView#键盘抬起事件
 * @fires BaseView#地图视图改变事件
 * @param {Object} options 构造参数
 * @param {Map}  [options.Map]  图层管理容器对象
 * @param {String} [options.viewId] 二维场景视图的容器(html的div标签)ID
 * @param {Number} [options.minZoom = 0] 最小缩放级数,在此设置则所有图层都不会浏览小于该级数的图片
 * @param {Number} [options.maxZoom = 19] 最大缩放级数,在此设置则所有图层都不会浏览大于该级数的图片
 * @param {Number} [options.zoom = 1] 初始化二维场景视图时显示级数
 * @param {Boolean} [options.attributionControl = false] 是否显示右下角水印
 * @param {Boolean} [options.zoomControl = true] 是否显示缩放控件
 * @param {Boolean | String} [options.doubleClickZoom = true] 是否允许双击鼠标左键缩放或者缩放至图层中心点
 * @param {Boolean} [options.dragging = true] 是否允许拖拽
 * @param {Number} [options.zoomSnap = 1] 当使用flyTo缩放至中心点时,缩放级数乘以的系数
 * @param {Number} [options.zoomDelta = 1] 当触发zoomIn或者zoomOut操作时,缩放级数乘以的系数
 * @param {Boolean} [options.trackResize = true] 是否允许图层大小随视窗变化
 * @param {Boolean} [options.keyboard = true] 是否允使用键盘的+/-号,来缩放地图
 * @param {Number} [options.keyboardPanDelta = 80] 使用键盘来平移或缩放地图时的系数,单位px
 * @param {Boolean | String} [options.scrollWheelZoom = true] 使用键盘来平移或缩放地图时的系数,单位px
 * @param {Number} [options.wheelDebounceTime = 40] 滚轮事件的触发事件,单位毫秒
 * @param {Number} [options.wheelPxPerZoomLevel = 60] 滚轮缩放时,地图缩放的像素单位,单位像素
 * @param {Boolean} [options.tapHold = true] 是否开启移动端,手指按压不放事件
 * @param {Number} [options.tapTolerance = 15] 手指有效触发范围,单位像素
 * @param {Boolean | String} [options.touchZoom = true] 是否启用手指两指缩放,当值为center,表示两只滑动,缩放至地图中心
 * @param {Boolean} [options.bounceAtZoomLimits = true] 当过最大或最小级数后不再缩放
 * @param {Boolean} [options.animation = true] 是否启用动画
 * @param {Point} [options.center = new Point({coordinates:[0,0]})] 地图视图中心点
 * @param {Extent} [options.extent = undefined] 地图视图可视范围
 * @param {Number} [options.scale = undefined] 地图视图比例尺
 * @param {Number} [options.maxScale = undefined] 最大比例尺
 * @param {Number} [options.minScale = undefined] 最小比例尺
 * @param {Object} [options.popup = undefined] 地图弹框
 * @param {Number} [options.rotation = 0] 地图视图旋转选项。单位为度,默认为0,表示不进行旋转
 *
 * @summary <h5>支持如下方法:</h5>
 * <a href='#flyTo'>[1、视点跳转]</a><br/>
 * <a href='#destroy'>[2、销毁视图对象]</a><br/>
 * <a href='#getCenter'>[3、获取当前视图的中心点]</a><br/>
 * <a href='#getPixelCenter'>[3、获取当前视图的像素中心点]</a><br/>
 * <a href='#getZoom'>[4、获取当前缩放级数]</a><br/>
 * <a href='#getExtent'>[5、获取当前视图的地理范围]</a><br/>
 * <a href='#getPixelWorldExtent'>[6、获取当前视图的像素范围]</a><br/>
 * <a href='#getMinZoom'>[7、获取最小缩放级数]</a><br/>
 * <a href='#getMaxZoom'>[8、获取最大缩放级数]</a><br/>
 * <a href='#getSize'>[9、获取当前视图容器的宽高]</a><br/>
 * <a href='#toJSON'>[10、导出场景视图的配置文件]</a><br/>
 * <a href='#clone'>[11、克隆并返回一个新的场景视图对象]</a><br/>
 * <a href='#fromJSON'>[12、通过json构造并返回一个新的场景视图对象]</a><br/>
 * <a href='#takeScreenshot'>[13、屏幕快照]</a><br/>
 * [14、注册事件]{@link Evented#on}<br/>
 * [15、移除事件]{@link Evented#off}<br/>
 * <a href='#toMap'>[15、屏幕像素坐标点转地理坐标点]</a><br/>
 * <a href='#toScreen'>[16、地理坐标点转屏幕像素坐标点]</a><br/>
 * <a href='#hitTest'>[17、穿透检测]</a><br/>
 * <a href='#getLayer'>[18、根据实际图层对象查询并返回基础图层]</a><br/>
 * <a href='#getScale'>[19、获取当前比例尺]</a><br/>
 *
 * @example <caption><h7 id='MapView'>初始化一个二维场景视图</h7></caption>
 * // ES5引入方式
 * const { Map, MapView } = Zondy
 * // ES6引入方式
 * import { Map, MapView } from "@mapgis/webclient-leaflet-plugin"
 * // 初始化图层管理容器
 * const map = new Map();
 * // 初始化地图视图对象
 * const mapView = new MapView({
 *   // 二维场景视图的容器(html的div标签)ID
 *   viewId: "二维场景视图的容器的id",
 *   // 图层管理容器
 *   map: map
 * });
 */
class MapView extends BaseView {
  constructor(options) {
    super(options)
    // eslint-disable-next-line no-param-reassign
    options = defaultValue(options, {})
    this._options = options
    /**
     * 地图视图中心点
     * @member {Point} MapView.prototype.center
     */
    // 统一center的类型
    this._center = defaultValue(
      options.center,
      new Point({ coordinates: [0, 0] })
    )
    if (
      this._center &&
      Array.isArray(this._center) &&
      this._center.length > 1
    ) {
      this._center = new Point({
        coordinates: [this._center[0], this._center[1]]
      })
    }
    /**
     * 地图坐标系
     * @member {Number} MapView.prototype.zoom
     */
    this.crs = undefined // defaultValue(options.crs, 'EPSG3857')
    /**
     * 地图视图可视范围
     * @member {Extent} MapView.prototype.extent
     */
    this._extent = defaultValue(options.extent, undefined)
    /**
     * 是否启用视角跳转动画
     * @member {Boolean} MapView.prototype.animation
     */
    this.animation = defaultValue(options.animation, true)
    /**
     * 地图视图宽度
     * @readonly
     * @member {Number} MapView.prototype.width
     */
    this.width = undefined
    /**
     * 地图视图高度
     * @readonly
     * @member {Number} MapView.prototype.height
     */
    this.height = undefined
    /**
     * 地图层级
     * @member {Number} MapView.prototype.zoom
     */
    this._zoom = defaultValue(options.zoom, 0)
    this._resolution = 0 // defaultValue(options.resolution, 0)
    /**
     * 地图视图比例尺
     * @member {Number} MapView.prototype.scale
     */
    this._scale = defaultValue(options.scale, undefined)
    /**
     * 地图视图范围
     * @member {Extent} MapView.prototype.extent
     */
    this._extent = defaultValue(options.extent, undefined)
    /**
     * 地图视图最大比例尺
     * @member {Number} MapView.prototype.maxScale
     */
    this.maxScale = defaultValue(options.maxScale, undefined)
    /**
     * 地图视图最小比例尺
     * @member {Number} MapView.prototype.minScale
     */
    this.minScale = defaultValue(options.minScale, undefined)
    /**
     * 地图弹框popup
     * @member {Popup} MapView.prototype.popup
     */
    this.popup = defaultValue(options.popup, undefined)
    /**
     * 视图渲染方式是否为canvas
     * @member {Boolean} MapView.prototype.preferCanvas
     */
    this.preferCanvas = defaultValue(options.preferCanvas, false)
    // if (typeof this.crs === 'string') {
    //   if (L.CRS[this.crs]) {
    //     this.crs = L.CRS[this.crs]
    //   } else {
    //     this.crs = L.CRS.EPSG4326
    //   }
    // }
    /**
     * 视图空间参考系
     * @readonly
     * @member {SpatialReference} MapView.prototype._spatialReference
     */
    this._spatialReference = undefined
    this._referenceLayer = undefined
    /**
     * 是否锁定视图空间参考系
     * @member {Boolean} MapView.prototype.spatialReferenceLocked
     */
    this.spatialReferenceLocked = defaultValue(
      options.spatialReferenceLocked,
      false
    )
    /**
     * 地图视图旋转选项。单位为度,默认为0,表示不进行旋转
     * @member {Boolean} MapView.prototype.rotation
     */
    this._rotation = defaultValue(options.rotation, 0)
    // 是否重置地图视图的坐标系
    this._isCustomCrs = false
    // 是否绑定Leaflet
    this._map._initLeafletView = true
    // 屏幕打印对象
    this._screenshot = null
    // 初始化地图引擎
    // this._initView(false, this._options)
  }

  /**
   * 视点跳转<a id='flyTo'></a>
   * @param options 跳转参数
   * @param {Array}  [options.center]  跳转中心点
   * @param {Number}  [options.zoom = 1]  地图层级
   * @param {Extent}  [options.extent]  按范围跳转
   * @example <caption><h7>中心点跳转示例</h7></caption>
   * // ES5引入方式
   * const { Map, MapView } = Zondy
   * // ES6引入方式
   * import { Map, MapView } from "@mapgis/webclient-leaflet-plugin"
   * // 初始化图层管理容器
   * map = new .Map();
   * // 初始化地图视图对象
   * mapView = new MapView({
   *   // 视图id
   *   viewId: "view-id",
   *   // 图层管理容器
   *   map: map
   * });
   * // 视点跳转
   * mapView.flyTo({
   *   // 跳转中心点
   *   center: [{x}, {y}],
   *   // 地图层级
   *   zoom: {zoom}
   * });
   *
   * @example <caption><h7>按范围跳转示例</h7></caption>
   * // ES5引入方式
   * const { Map, MapView } = Zondy
   * const { Extent } = Zondy.Geometry
   * // ES6引入方式
   * import { Map, MapView, Extent } from "@mapgis/webclient-leaflet-plugin"
   * // 初始化图层管理容器
   * map = new Map();
   * // 初始化地图视图对象
   * mapView = new MapView({
   *   // 视图id
   *   viewId: "view-id",
   *   // 图层管理容器
   *   map: map
   * });
   * mapView.flyTo({
   *   // 范围几何
   *   extent: new Extent({
   *      "xmin":10,
   *      "xmax":210,
   *      "ymin":0,
   *      "ymax":100,
   *   })
   * });
   *
   * @example <caption><h7>按范围跳转示例-拿到图层信息后跳转</h7></caption>
   * // ES5引入方式
   * const { IGSMapImageLayer } = Zondy.Layer
   * // ES6引入方式
   * import { IGSMapImageLayer } from "@mapgis/webclient-leaflet-plugin"
   * const igsMapImageLayer = new IGSMapImageLayer({
   *   url: 'http://192.168.82.89:8089/igs/rest/services/Map/Hubei4326/MapServer'
   * });
   * map.add(igsMapImageLayer);
   * // 图层加载完毕
   * igsMapImageLayer.on('layer-view-created', function (result) {
   *   console.log("加载完毕:", result.layer)
   *   //视点跳转
   *   mapView.flyTo({
   *     extent: result.layer.extent
   *   });
   * })
   */
  flyTo(options) {
    options = defaultValue(options, {})
    const zoom = defaultValue(options.zoom, 1)

    // 获取center
    const center = this._getFlyToCenter(options)

    // 获取extent
    const extent = this._getFlyToExtent(options)

    if (this._innerView) {
      if (center) {
        this._innerView.setView(
          [center.coordinates[1], center.coordinates[0]],
          zoom
        )
      } else if (extent) {
        this._innerView.fitBounds([
          [extent.ymin, extent.xmin],
          [extent.ymax, extent.xmax]
        ])
      }
    }
  }

  /**
   * 从extent获取经纬度范围
   * @private
   * @param {Extent} extent 范围对象
   * @return {Object} 经纬度范围
   * */
  _getLatLngFromExtent(extent) {
    const spatialReference = new SpatialReference('EPSG:4326')
    const newExtent = Projection.project(extent, spatialReference)
    const northEastLatlng = new L.latLng(newExtent.ymax, newExtent.xmax)
    const southWestLatlng = new L.latLng(newExtent.ymin, newExtent.xmin)

    return {
      northEastLatlng,
      southWestLatlng
    }
  }

  /**
   * 从center获取经纬度范围
   * @private
   * @param {Point} point 范围对象
   * @return {Object} 地理坐标
   * */
  _getLatLngFromPoint(point) {
    const spatialReference = new SpatialReference('EPSG:4326')
    const newCenter = Projection.project(point, spatialReference)
    const centerLatlng = new L.latLng(
      newCenter.coordinates[1],
      newCenter.coordinates[0]
    )
    return centerLatlng
  }

  /**
   * 初始化地图视图事件
   * @private
   * */
  _initViewEvent() {
    const self = this
    // 定义一个变量存储单击触发事件
    this._clickStore = null
    // 定义一个变量存储拖拽事件参数
    this._dragParams = { isDrag: false, origin: null, action: '', button: null }

    // 地图视图加载完毕事件
    self.fire(ViewEventType.loaded, {})

    // leaflet注册点击事件
    this._innerView.on('click', (event) => {
      // 单击触发事件,300ms延迟
      self._clickStore = setTimeout(function () {
        // 发送左键单击点击事件
        if (self._clickStore) {
          self.fire(ViewEventType.click, MapViewUtil.getMouseEvent(event, self))
        }
      }, 300)
      // 发送立即点击事件
      self.fire(
        ViewEventType.immediateClick,
        MapViewUtil.getMouseEvent(event, self)
      )
    })

    // 注册鼠标右键按下事件
    this._innerView.on('contextmenu', (event) => {
      // 单击触发事件,300ms延迟
      self._clickStore = setTimeout(function () {
        // 发送鼠标右键按下事件
        if (self._clickStore) {
          self.fire(ViewEventType.click, MapViewUtil.getMouseEvent(event, self))
        }
      }, 300)
      // 发送立即点击事件
      self.fire(
        ViewEventType.immediateClick,
        MapViewUtil.getMouseEvent(event, self)
      )
    })

    // 注册双击事件
    this._innerView.on('dblclick', (event) => {
      // 单击事件清理
      if (self._clickStore) {
        clearTimeout(this._clickStore)
        self._clickStore = null
      }
      // 发送双击事件
      self.fire(
        ViewEventType.doubleClick,
        MapViewUtil.getMouseEvent(event, self)
      )
    })

    // 注册鼠标按下事件
    this._innerView.on('mousedown', (event) => {
      // 发送鼠标按下事件
      self.fire(
        ViewEventType.pointerDown,
        MapViewUtil.getMouseEvent(event, self)
      )
      // 发送鼠标拖拽开始事件
      if (!this._dragParams.isDrag) {
        this._dragParams.origin = {
          x: event.originalEvent.layerX,
          y: event.originalEvent.layerY
        }
        this._dragParams.button = event.originalEvent.button
        this._dragParams.action = 'mouse-down'
        this._dragParams.isDrag = false
      }
    })

    // 注册鼠标抬起事件
    this._innerView.on('mouseup', (event) => {
      // 发送鼠标抬起事件
      self.fire(ViewEventType.pointerUp, MapViewUtil.getMouseEvent(event, self))
      // 发送鼠标拖拽结束事件
      if (self._dragParams.isDrag && this._dragParams.action === 'update') {
        this._dragParams.action = 'end'
        self.fire(
          ViewEventType.drag,
          MapViewUtil.getDragEvent(
            event,
            self,
            self._dragParams.origin,
            this._dragParams.action
          )
        )
      }
      self._dragParams = {
        isDrag: false,
        origin: null,
        action: '',
        button: null
      }
    })

    // 注册鼠标移动事件
    this._innerView.on('mousemove', (event) => {
      // 发送鼠标移动视图事件
      self.fire(
        ViewEventType.pointerMove,
        MapViewUtil.getMouseEvent(event, self)
      )
      // 发送鼠标拖拽中事件
      if (self._dragParams.isDrag) {
        self.fire(
          ViewEventType.drag,
          MapViewUtil.getDragEvent(
            event,
            self,
            self._dragParams.origin,
            self._dragParams.action,
            self._dragParams.button
          )
        )
        self._dragParams.action = 'update'
      } else if (this._dragParams.action === 'mouse-down') {
        this._dragParams.action = 'start'
        self._dragParams.isDrag = true
      }
    })

    // 注册键盘按下事件
    this._innerView.on('keydown', (event) => {
      // 发送键盘按下事件
      self.fire(
        ViewEventType.keyDown,
        MapViewUtil.getKeyEvent(event, self, ViewEventType.keyDown)
      )
    })

    // 注册键盘抬起事件
    this._innerView.on('keyup', (event) => {
      // 发送键盘抬起事件
      self.fire(
        ViewEventType.keyUp,
        MapViewUtil.getKeyEvent(event, self, ViewEventType.keyUp)
      )
    })

    // 注册视图开始改变事件
    this._innerView.on('movestart', () => {
      self.stationary = false
    })

    // 注册zoom变化事件
    this._innerView.on('zoom', (event) => {
      self._map.layers.forEach(function (layer) {
        switch (layer.type) {
          case LayerType.igsFeature:
          case LayerType.graphics:
            showLayerByScale(self, layer)
            break
          default:
            break
        }
      })
      // 发送zoom变化事件
      self.fire(ViewEventType.zoom, {
        zoom: event.target._zoom,
        event
      })
    })
    // 注册zoom end事件
    this._innerView.on('zoomend', (event) => {
      // 更新地图视野范围,比例尺
      self._zoomEndHandler()
      // 发送地图视图改变事件
      self.fire(
        ViewEventType.viewChange,
        MapViewUtil.getViewChangeEvent(event, self)
      )
    })

    // 注册地图移动完成事件
    this._innerView.on('moveend', (event) => {
      // 更新地图视中心
      self._moveEndHandler()
      // 发送地图视图改变事件
      self.fire(
        ViewEventType.viewChange,
        MapViewUtil.getViewChangeEvent(event, self)
      )
      // self.fire(ViewEventType.mapMove, event)
      self.stationary = true
    })

    // 注册地图大小变化事件
    this._innerView.on('resize', (event) => {
      // 发送地图初始化后触发的事件
      self.fire(ViewEventType.resize, {
        oldWidth: event.oldSize.x,
        oldHeight: event.oldSize.y,
        width: event.newSize.x,
        height: event.newSize.y
      })
      // 更新地图窗口宽高
      self._initSize()
    })
  }

  /**
   * 设置(鼠标)操作地图事件是否可用
   * @private
   * @param {String} actionType 操作类型
   * @param {Boolean} isEnable 是否可用
   * */
  _mapActionControl(actionType, isEnable) {
    if (!this._innerView) return
    if (actionType === 'drag-pan') {
      if (isEnable) {
        this._innerView.dragging.enable()
      } else {
        this._innerView.dragging.disable()
      }
    } else if (actionType === 'double-click-zoom') {
      if (isEnable) {
        this._innerView.doubleClickZoom.enable()
      } else {
        this._innerView.doubleClickZoom.disable()
      }
    }
  }

  /**
   * 初始化一个地图视图对象
   * @param isReset 是否是重置视图
   * @private
   * */
  _initView(isReset, options) {
    this._innerView = L.map(this._viewId, {
      // 是否使用Canvas渲染方式
      preferCanvas: this.preferCanvas,
      // 是否显示右下角水印
      attributionControl: defaultValue(options.attributionControl, false),
      // 是否显示缩放控件
      zoomControl: defaultValue(options.zoomControl, true),
      // 是否允许双击鼠标左键缩放或者缩放至图层中心点
      doubleClickZoom: defaultValue(options.doubleClickZoom, true),
      // 是否允许拖拽
      dragging: defaultValue(options.dragging, true),
      // 当使用flyTo缩放至中心点时,缩放级数乘以的系数
      zoomSnap: defaultValue(options.zoomSnap, 1),
      // 当触发zoomIn或者zoomOut操作时,缩放级数乘以的系数
      zoomDelta: defaultValue(options.zoomDelta, 1),
      // 是否允许图层大小随视窗变化
      trackResize: defaultValue(options.trackResize, true),
      // 是否允使用键盘的+/-号,来缩放地图或者使用方向键来平移地图
      keyboard: defaultValue(options.keyboard, true),
      // 使用键盘来平移或缩放地图时的系数,单位px
      keyboardPanDelta: defaultValue(options.keyboardPanDelta, 80),
      // 是否允许使用鼠标滚轮来缩放地图,当值为center时,表示使用滚轮缩放至地图中心点
      scrollWheelZoom: defaultValue(options.scrollWheelZoom, true),
      // 滚轮事件的触发事件,单位毫秒
      wheelDebounceTime: defaultValue(options.wheelDebounceTime, 40),
      // 滚轮缩放时,地图缩放的像素单位,单位像素
      wheelPxPerZoomLevel: defaultValue(options.wheelDebounceTime, 60),
      // 是否开启移动端,手指按压不放事件
      tapHold: defaultValue(options.tapHold, true),
      // 手指有效触发范围,单位像素
      tapTolerance: defaultValue(options.tapTolerance, 15),
      // 是否启用手指两指缩放,当值为center,表示两只滑动,缩放至地图中心
      touchZoom: defaultValue(options.touchZoom, true),
      // 当过最大或最小级数后不再缩放
      bounceAtZoomLimits: defaultValue(options.bounceAtZoomLimits, true),
      // 图层空间参考系
      crs: this.crs,
      // 图层中心点
      center:
        this._center && this._center.coordinates
          ? [this._center.coordinates[1], this._center.coordinates[0]]
          : [0, 0],
      // 可视范围
      extent: defaultValue(this._extent, undefined),
      // 最大缩放级数
      maxZoom: this.maxZoom,
      // 最小缩放级数
      minZoom: this.minZoom,
      // 初始化时显示级数
      zoom: this._zoom,
      // 是否启用视点跳转动画
      zoomAnimation: this.animation,
      // Config
      config: Config,
      // 是否进行旋转,目前对外没有constraints,默认设置为true
      rotate: true,
      // 旋转bearing,单位为度
      bearing: this._rotation
    })
    // 初始化ui容器
    this.ui = new UI({
      view: this
    })
    // 初始化视图事件
    this._initViewEvent()
    // 初始化可视范围
    if (this._extent && !isReset) {
      this.flyTo({ extent: this._extent })
    }
    // 初始化比例尺,extent定位优先于scale,center定位
    if (!this._extent && this._scale && this._center && !isReset) {
      this._initScale()
    }
    // 获取视图大小
    this._initSize()
    // 初始化最大比例尺和最小比例尺
    this._initScaleLimit()
    // 初始化地图视图弹框,目前仅支持location方式打开弹框
    this._initPopup()
  }

  // 设置spatialReference
  _setSpatialReference() {
    // 如果basemap有坐标系,则view坐标系取basemap坐标系的值。
    // 没有,则view根据加载的图层动态坐标系的值。
    let spatialReference = null
    let referenceLayer = null
    if (this._map.basemap && this._map.basemap.spatialReference) {
      spatialReference = this._map.basemap.spatialReference
      referenceLayer = this._map.basemap._referenceLayer
    } else if (this._map.layers) {
      // const target = Utils.getSpatialReference(this._map.layers)
      const target = TileInfoUtil.getSpatialReference(this._map.layers)
      spatialReference = target.spatialReference
      referenceLayer = target.referenceLayer
    }
    // 没有获取到图层参考系,则直接返回
    if (!spatialReference) return
    // 当view上未设置空间参考系锁定,
    // 或空间参考系锁定但没有spatialReference时,则获取新的spatialReference
    if (
      !this.spatialReferenceLocked ||
      (this.spatialReferenceLocked && !this._spatialReference)
    ) {
      this._spatialReference = spatialReference
      this._referenceLayer = referenceLayer
      // 如果是MapImageLayer,则重置坐标系为当前坐标系
      if (this._spatialReference) {
        const isBasemapLayer = referenceLayer._isBasemapLayer
        TileInfoUtil.setLayerSpatialReference(
          this._map.layers,
          this._spatialReference,
          isBasemapLayer
        )
      }
      // 设置crs
      this.setCrs(referenceLayer)
    }
  }

  setCrs(referenceLayer) {
    const crs = this.getCrsByReferenceLayer(referenceLayer)
    let isNew = false
    let isFirst = false
    if (this.crs) {
      // 比较被设置的新Crs和当前this.crs是否相同。
      // 判断标准 crs.option的resolutions,origin,bounds是否相等。
      if (crs !== null) {
        const newCrsInfo = {
          wkid: crs.code.split(':')[1]
        }
        const currentCrsInfo = {
          wkid: this.crs.code.split(':')[1]
        }
        if (newCrsInfo.wkid === currentCrsInfo.wkid) {
          // ArcgisMapImage图层临时处理,服务数据extent有问题,暂为解决。4326、3857参考系构建对应的Leaflet参考系对象,
          // 因此没有options,不参与对比bounds、resolutions,暂时组织比较wkid
          // 问题解决后,统一比较 crs.options,该if条件可移除
          if (
            (newCrsInfo.wkid === '4326' || newCrsInfo.wkid === '3857') &&
            !crs.options
          ) {
            isNew = false
          }
          if (crs.options) {
            const newCrsOptions = crs.options
            newCrsInfo.resolutions = newCrsOptions.resolutions
            newCrsInfo.origin = newCrsOptions.origin
            newCrsInfo.bounds = newCrsOptions.bounds
            currentCrsInfo.resolutions = this.crs.options.resolutions
            currentCrsInfo.origin = this.crs.options.origin
            currentCrsInfo.bounds = this.crs.options.bounds
            if (
              JSON.stringify(newCrsInfo.resolutions) !==
                JSON.stringify(currentCrsInfo.resolutions) ||
              JSON.stringify(newCrsInfo.origin) !==
                JSON.stringify(currentCrsInfo.origin) ||
              JSON.stringify(newCrsInfo.bounds) !==
                JSON.stringify(currentCrsInfo.bounds)
            ) {
              isNew = true
            }
          }
        } else {
          isNew = true
        }
      }
    } else if (!this.crs && crs) {
      isNew = true
      isFirst = true
    }
    if (isNew) {
      this.crs = crs
      if (referenceLayer.extent) {
        const extent = new Extent({
          xmin: referenceLayer.extent.xmin,
          xmax: referenceLayer.extent.xmax,
          ymin: referenceLayer.extent.ymin,
          ymax: referenceLayer.extent.ymax,
          spatialReference: referenceLayer.spatialReference
        })
        const latLng = this._getLatLngFromExtent(extent)
        this._center = new Point({
          coordinates: [
            (latLng.southWestLatlng.lng + latLng.northEastLatlng.lng) / 2,
            (latLng.southWestLatlng.lat + latLng.northEastLatlng.lat) / 2
          ]
        })
      }
      const layers = []
      const baseLayers = []
      this._map.layers.items.forEach((layer) => {
        // 如果是当前图层load完之后触发的重置方法,则不需要重载图层

        // 图层信息已加载完全并且不是第一次设置坐标系,则需要重新加载
        // 图层信息没加载完全并且是第一次设置坐标系,则需要重新加载
        // 图层是第一次设置坐标系,并且是graphicsLayer(不管graphicsLayer是否loaded),则需要重新加载
        // 图层是参考图层,extent没变,则不需要重新加载。图层是参考图层,extent变了,则需要重新加载
        if (this._loadedLayer && layer.id === this._loadedLayer.id) {
          return
        }
        if (
          (layer.loaded && !isFirst) ||
          (!layer.loaded && isFirst) ||
          (isFirst && layer.type === LayerType.graphics)
        ) {
          layers.push(layer)
          if (layer._isBasemapLayer) {
            layer._isBasemapLayer = false
            baseLayers.push(layer)
          }
        }
      })
      this._map._canResetView = false
      this._map.removeMany(layers)
      baseLayers.forEach((layer) => {
        layer._isBasemapLayer = true
      })
      this._reCreateView()
      this._map.addMany(layers)
      this._map._canResetView = true
      if (isFirst) {
        referenceLayer.on('layerview-created', () => {
          // 初始化可视范围
          if (this._extent) {
            this.flyTo({ extent: this._extent })
          }
          // 初始化比例尺,extent定位优先于scale,center定位
          if (!this._extent && this._scale && this._center) {
            this._initScale()
          }
          // 获取视图大小
          this._initSize()
          // 初始化最大比例尺和最小比例尺
          this._initScaleLimit()
          // 初始化地图视图弹框,目前仅支持location方式打开弹框
          this._initPopup()
        })
      }
    }
  }

  /**
   * 删除当前视图,重新创建一个视图
   * @private
   * */
  _reCreateView() {
    if (this._innerView) {
      this._innerView.remove()
      this._resetExtent()
    }
    this._initView(true, this._options)
  }

  /**
   * 根据crs重新创建extent,center等属性
   * @private
   * */
  _resetExtent() {
    if (this._extent) {
      this._extent = undefined
      this._zoom = 0
    }
  }

  /**
   * 根据图层的spatialReference对象,生成一个坐标系,在leaflet上一定要resolutions、origin、extent以及椭球体参数,
   * 才可以创建一个坐标系,瓦片或者WMTS自带resolutions、origin、extent,
   * 矢量或者WMS需要从extent上计算出resolutions和origin,计算操作在基础图层中完成
   * @private
   * @param {Layer} layer 图层
   * @return {CRS} 地图视图的坐标系对象
   * */
  _getCrs(layer) {
    const spatialReference = layer.spatialReference

    // 投影范围
    const layerExtent = layer.spatialReference.extent || layer.extent
    const extent = Extent.fromJSON(layerExtent.toJSON())

    const isSameSpatialReference = function (
      spatialReference1,
      spatialReference2
    ) {
      let isSame = false
      if (
        [3857, 102113, 102100, 900913].indexOf(spatialReference1.wkid) > -1 &&
        [3857, 102113, 102100, 900913].indexOf(spatialReference2.wkid) > -1
      ) {
        isSame = true
      } else if (
        [4326, 4490, 4610, 4214].indexOf(spatialReference1.wkid) > -1 &&
        [4326, 4490, 4610, 4214].indexOf(spatialReference2.wkid) > -1
      ) {
        isSame = true
      } else if (spatialReference1.wkid === spatialReference2.wkid) {
        isSame = true
      }
      return isSame
    }
    this._map.layers.forEach((layerItem) => {
      // 如果当前layer是基础底图,则不受普通图层的参考系范围影响,但也会受其他基础底图图层的参考系范围影响
      if (layer._isBasemapLayer && !layerItem._isBasemapLayer) {
        return
      }
      if (
        layerItem.spatialReference &&
        layer.spatialReference &&
        isSameSpatialReference(
          layerItem.spatialReference,
          layer.spatialReference
        ) &&
        layerItem.loaded
      ) {
        if (extent.xmin > layerItem.extent.xmin) {
          extent.xmin = layerItem.extent.xmin
        }
        if (extent.ymin > layerItem.extent.ymin) {
          extent.ymin = layerItem.extent.ymin
        }
        if (extent.xmax < layerItem.extent.xmax) {
          extent.xmax = layerItem.extent.xmax
        }
        if (extent.ymax < layerItem.extent.ymax) {
          extent.ymax = layerItem.extent.ymax
        }
      }
    })

    let tileInfo = layer.tileInfo
    // 处理 瓦片图层、arcgis瓦片图层、矢量瓦片图层
    if (layer.type === LayerType.wmts) {
      tileInfo = TileInfoUtil.parseWMTSTileInfo(layer)
    } else if (layer.type === LayerType.wms) {
      tileInfo = TileInfoUtil.parseWMSTileInfo(layer, extent)
    } else if (
      // igsFeature、wfs、geojson图层是矢量图形,图层没有tile size概念,
      // 但Leaflet引擎需要构造分辨率级,给定一个256默认tile size,用于计算分辨率级。
      // igsMapImage图层,如果是图像渲染模式则和上序逻辑相同。如果是瓦片渲染模式,tileSize取图层上设置的瓦片宽高。
      // arcgisMapImage服务对接有暂时问题,暂不自定义计算,直接取arcgisMapImage的tileInfo的分辨率
      layer.type === LayerType.igsMapImage ||
      // layer.type === LayerType.arcgisMapImage ||
      layer.type === LayerType.igsFeature ||
      layer.type === LayerType.wfs ||
      layer.type === LayerType.geojson
    ) {
      tileInfo = TileInfoUtil.parseMapImageTileInfo(layer, extent)
    }
    // igsMapImage图层临时处理,目前根据arcgisMapImage的extent、origin和tileInfo.lods构造出来的参考系有问题。
    // 问题解决后,统一走 tileInfo = TileInfoUtil.parseMapImageTileInfo(layer)逻辑。
    if (layer.type === LayerType.arcgisMapImage) {
      if (
        [3857, 102113, 102100, 900913].indexOf(layer.spatialReference.wkid) > -1
      ) {
        return L.CRS.EPSG3857
      } else if (
        [4326, 4490, 4610, 4214].indexOf(layer.spatialReference.wkid) > -1
      ) {
        return L.CRS.EPSG4326
      } else {
        tileInfo = TileInfoUtil.parseMapImageTileInfo(layer)
      }
    }

    // 原点
    const origin = Point.toCoordinates(tileInfo.origin)
    // 获取分辨率数组
    const resolutions = tileInfo.lods.map((lod) => lod.resolution)
    const length = resolutions.length
    // 如果数组长度小于20,则将数组最后一个除以2,将数组扩充为20,否则地图视图无法继续缩放
    if (length < 20) {
      let lastResolutions = resolutions[length - 1]
      let lastLod = tileInfo.lods[length - 1]
      for (let i = 0; i < 20 - length; i++) {
        resolutions.push(lastResolutions / 2)
        lastLod = {
          resolution: lastResolutions / 2,
          scale: lastLod.scale / 2,
          level: tileInfo.lods.length
        }
        tileInfo.lods.push(lastLod)
        lastResolutions /= 2
      }
    }
    // 创建一个坐标系对象
    return new CRS({
      // 指定wkid,wkid的优先级比wkt高,和igs保持一致
      wkid: spatialReference.wkid,
      // 指定wkt
      wkt: spatialReference.wkt,
      resolutions,
      lods: tileInfo.lods,
      origin,
      extent,
      // 加载矢量瓦片特殊逻辑,需要传入矢量瓦片tileSize(mapboxgl对于初级瓦片的要求),仅影响矢量瓦片显示
      mapboxTileSize: spatialReference._mapboxTileSize
    })
  }

  /**
   * @description 创建图层视图
   * @private
   * @param {Layer} layer 图层
   * @return {LayerView| null}
   */
  _createLayerView(layer) {
    const layerView = getLayerView(this, layer) || null
    return layerView
  }

  /**
   * @description 事件处理器,重载基类方法
   * @private
   */
  _processEvent(event) {
    super._processEvent(event)
    if (event.type === MapEventType.layerRemove) {
      if (
        !this._map._isRemoveMany &&
        !this._map._isRemoveAll &&
        this._map._canResetView
      ) {
        this._setSpatialReference()
      }
    }
    if (
      event.type === MapEventType.layerRemoveMany &&
      this._map._canResetView
    ) {
      if (event.layers) {
        this._setSpatialReference()
      }
    }
  }

  /**
   * @description 通过参考系图层获取CRS对象
   * @private
   * @param {Layer} referenceLayer 图层加载完毕后的对象
   * @return {CRS} CRS对象
   */
  getCrsByReferenceLayer(referenceLayer) {
    let crs = null
    if (
      !referenceLayer ||
      !referenceLayer.spatialReference ||
      !referenceLayer.loaded
    ) {
      return crs
    }
    // 根据基础图层对象上的spatialReference,创建一个自定义坐标系
    // (4326、3857也更具图层信息构造Crs,而不是定义为Leaflet的默认4326、3857Crs)
    crs = this._getCrs(referenceLayer)
    return crs
  }

  /**
   * @description 初始化获取视图尺寸大小
   * @private
   */
  _initSize() {
    const size = this._innerView.getSize()
    this.width = size.x
    this.height = size.y
  }

  /**
   * 等待图层加载完毕,重写BaseView类的该方法
   * @param {Layer} layer 基础图层对象
   * @param {Object} result 图层加载完毕后的对象
   * @param {Function} fireCreatedError 创建失败回调
   * */
  _waitLayerLoaded(layer, result, fireCreatedError) {
    const self = this
    // 是提供坐标系的图层,则加载图层
    if (layer._isSRLayer) {
      const promise = self._addLayer(result, event)
      self._layerLoaded(promise, layer, result, fireCreatedError)
    }
    // 不是的则等待图层加载完毕
    else {
      if (layer._interval) {
        clearInterval(layer._interval)
        layer._interval = undefined
      }
      layer._interval = setInterval(function () {
        const srLayer = self._map._getSRLayer()
        if (!srLayer || (srLayer && srLayer.loaded)) {
          clearInterval(layer._interval)
          layer._interval = undefined
          const promise = self._addLayer(result, event)
          self._layerLoaded(promise, layer, result, fireCreatedError)
        }
      }, 50)
    }
  }

  /**
   * 销毁视图对象<a id='destroy'></a>
   * */
  destroy() {
    this._innerView.remove()
  }

  /**
   * 获取当前视图的中心点(经纬度中心点)<a id='getCenter'></a>
   * @return {Object} 中心点对象
   * */
  getCenter() {
    const center = this._innerView.getCenter()
    let centerCrs = new Point({ coordinates: [center.lng, center.lat] })
    this._center = centerCrs
    const spatialReference = new SpatialReference(this.crs.code)
    if (!spatialReference.isGeographic) {
      centerCrs = Projection.project(centerCrs, spatialReference)
    }
    return centerCrs
  }

  /**
   * <a id='setCenter'></a>
   * 设置缩视野中心<a id='setCenter'></a>
   * @private
   * @param {Point} value 视图中心
   * */
  setCenter(value) {
    if (!this._innerView) return
    if (Array.isArray(value) && value.length > 1) {
      this._center = new Point({
        coordinates: [value[0], value[1]],
        spatialReference: new SpatialReference(this.crs.code)
      })
      this.flyTo({ zoom: this._zoom, center: this._center })
    } else if (value instanceof Point) {
      this._center = value
      this.flyTo({ zoom: this._zoom, center: this._center })
    }
  }

  /**
   * 获取当前缩放级数<a id='getZoom'></a>
   * @return {Number} 当前缩放级数
   * */
  getZoom() {
    this._zoom = this._innerView.getZoom()
    return this._zoom
  }

  /**
   * <a id='setZoom'></a>
   * 设置缩放级数
   * @private
   * @param {Number} value 视图层级
   * */
  setZoom(value) {
    if (!this._innerView) return

    this._zoom = value
    this.flyTo({ zoom: value, center: this._center })
  }

  /**
   * 获取最小缩放级数<a id='getMinZoom'></a>
   * @return {Number} 最小缩放级数
   * */
  getMinZoom() {
    return this._innerView.getMinZoom()
  }

  /**
   * @description 获取当前视图容器的宽高,单位像素<a id='getSize'></a>
   * @return {Object} 当前视图容器的宽高对象
   * */
  getSize() {
    return this._innerView.getSize()
  }

  /**
   * 获取最大缩放级数<a id='getMaxZoom'></a>
   * @return {Number} 最大缩放级数
   * */
  getMaxZoom() {
    return this._innerView.getMaxZoom()
  }

  /**
   * 获取当前视图的宽高范围,单位像素<a id='getPixelExtent'></a>
   * @return {Object} 视图宽高对象
   * */
  getPixelExtent() {
    return this._innerView.getPixelBounds()
  }

  /**
   * 获取当前视图的像素中心点<a id='getPixelCenter'></a>
   * @return {Object} 像素中心点对象
   * */
  getPixelCenter() {
    return this._innerView.getPixelOrigin()
  }

  /**
   * 获取当前视图的像素范围<a id='getPixelWorldExtent'></a>
   * @return {Object} 当前视图的像素范围
   * */
  getPixelWorldExtent() {
    return this._innerView.getPixelWorldBounds()
  }

  /**
   * 导出场景视图的配置文件<a id='toJSON'></a>
   * @return {Object} 导出的配置文件
   * */
  toJSON() {
    const _json = {}
    _json.extent = toJSON(this._extent, Extent)
    _json.center = toJSON(this._center, Point)
    _json.map = toJSON(this._map, Map)
    _json.viewId = this._viewId
    _json.animation = this.animation
    _json.width = this.width
    _json.height = this.height
    _json.zoom = this._zoom
    _json.scale = this._scale
    _json.maxScale = this.maxScale
    _json.minScale = this.minScale
    _json.preferCanvas = this.preferCanvas
    _json.spatialReferenceLocked = this.spatialReferenceLocked

    return _json
  }

  /**
   * 克隆并返回一个新的场景视图对象<a id='clone'></a>
   * @return {MapView} 一个新的场景视图对象
   * */
  clone() {
    return cloneObject(this)
  }

  /**
   * 通过json构造并返回一个新的场景视图对象<a id='fromJSON'></a>
   * @param {Object} json json对象
   * @return {MapView} 一个新的场景视图对象
   * */
  static fromJSON(json) {
    return new MapView(json)
  }

  /**
   * <a id='toMap'></a>
   * 屏幕像素坐标点转地理坐标点
   * @param {Object} screenPoint 屏幕像素坐标点,例如{ x: 900, y: 500 }
   * @return {Point} 地理坐标点
   * @example <caption><h5>屏幕像素坐标点转地理坐标示例</h5></caption>
   * // ES6引入方式
   * import { MapView } from "@mapgis/webclient-leaflet-plugin"
   * import { Map, Point, Extent } from "@mapgis/webclient-common"
   * // 初始化图层管理容器
   * const map = new Map();
   * // 初始化地图视图对象
   * const mapView = new MapView({
   *   // 视图id
   *   viewId: "mapgis-2d-viewer",
   *   // 图层管理容器
   *   map: map
   * });
   * const screenPoint = { x: 900, y: 500 }
   * mapView.toMap(screenPoint)
   * */
  toMap(screenPoint) {
    const latLngPoint = this._innerView.containerPointToLatLng(screenPoint)
    let geoPoint = new Point({
      coordinates: [latLngPoint.lng, latLngPoint.lat]
    })
    const spatialReference = new SpatialReference(this.crs.code)
    if (!spatialReference.isGeographic) {
      geoPoint = Projection.project(geoPoint, spatialReference)
    }
    return geoPoint
  }

  /**
   * <a id='toScreen'></a>
   * 地理坐标点转屏幕像素坐标点
   * @param  {Point}point 地理坐标点
   * @return {Object} 屏幕像素坐标点
   * @example <caption><h5>地理坐标点转屏幕像素坐标示例</h5></caption>
   * // ES6引入方式
   * import { MapView } from "@mapgis/webclient-leaflet-plugin"
   * import { Map, Point, Extent } from "@mapgis/webclient-common"
   * // 初始化图层管理容器
   * const map = new Map();
   * // 初始化地图视图对象
   * const mapView = new MapView({
   *   // 视图id
   *   viewId: "mapgis-2d-viewer",
   *   // 图层管理容器
   *   map: map
   * });
   * const geoPoint = new Point({ coordinates: [123, 23, 0] })
   * mapView.toScreen(geoPoint)
   * */
  toScreen(point) {
    let _point = Geometry.fromJSON(point)
    if (!_point.spatialReference.isGeographic) {
      _point = Projection.project(
        _point,
        new SpatialReference({
          wkid: 4326
        })
      )
    }
    const screenPoint = this._innerView.latLngToContainerPoint([
      _point.coordinates[1],
      _point.coordinates[0]
    ])
    return screenPoint
  }

  /**
   * <a id='takeScreenshot'></a>
   * @description 屏幕快照
   * @param {Object} [options = {}] 屏幕快照配置配置
   * @param {PictureFormat} [options.format = PictureFormat.png] 照片格式,支持png,jpeg格式
   * @param {String} [options.filename = 'screenshotFile'] 下载文件名
   * @param {Number} [options.width = undefined] 图片宽度
   * @param {Number} [options.height = undefined] 图片高度
   * @param {Number} [options.x = undefined] 图片原点x
   * @param {Number} [options.y = undefined] 图片原点y
   * @param {Boolean} [options.isDownload = true] 是否下载图片
   * @return {Object} 屏幕快照 {dataUrl String },且浏览器会下载图片
   * @example <caption><h7>屏幕快照</h7></caption>
   * // ES5引入方式
   * const { Map, MapView } = Zondy
   * const { PictureFormat } = Zondy.Enum
   * // ES6引入方式
   * import { Map, MapView } from "@mapgis/webclient-leaflet-plugin"
   * import { PictureFormat } from "@mapgis/webclient-common"
   * // 初始化图层管理容器
   * const map = new Map();
   * // 初始化地图视图对象
   * const mapView = new MapView({
   *   // 二维场景视图的容器(html的div标签)ID
   *   viewId: "二维场景视图的容器的id",
   *   // 图层管理容器
   *   map: map
   * })
   * // 设置屏幕快照参数
   * const screenshotOptions: {
   *    format: PictureFormat.png
   * }
   * // 开始屏幕快照
   * mapView.takeScreenshot(screenshotOptions).then((result) => {
   *   // 获取base64格式的url字符串
   *   console.log("dataUrl:", result.dataUrl)
   * })
   * */
  takeScreenshot(options) {
    options.view = this
    let screenshotResult = null
    if (!this._screenshot) {
      this._screenshot = new Screenshot(options)
      screenshotResult = this._screenshot._addView()
    } else {
      screenshotResult = this._screenshot._addView()
    }
    return screenshotResult
  }

  /**
   * 获取当前视图的地理范围<a id='getExtent'></a>
   * @return {Extent}  获取当前视图的地理范围
   * */
  getExtent() {
    if (!this._innerView) return
    const latLngBounds = this._innerView.getBounds()
    let extent = new Extent({
      xmin: latLngBounds._southWest.lng,
      ymin: latLngBounds._southWest.lat,
      xmax: latLngBounds._northEast.lng,
      ymax: latLngBounds._northEast.lat
    })
    const spatialReference = new SpatialReference(this.crs.code)
    if (!spatialReference.isGeographic) {
      extent = Projection.project(extent, spatialReference)
    }
    this._extent = extent
    return extent
  }

  /**
   * <a id='setExtent'></a>
   * 设置视野范围
   * @private
   * @param {Extent} value 范围对象
   * */
  setExtent(value) {
    if (!this._innerView) return
    this._extent = value
    this.flyTo({ extent: value })
  }

  /**
   * zoom end事件处理器
   * @private
   * */
  _zoomEndHandler() {
    // 更新地图视野范围
    this.getExtent()
    // 更新地图层级
    this.getZoom()
    // 更新地图比例尺
    this.getScale()
    this.getResolution()
  }

  /**
   * move end事件处理器
   * @private
   * */
  _moveEndHandler() {
    // 更新地图视野中心
    this.getCenter()
    // 更新地图视野范围
    this.getExtent()
  }

  /**
   * 初始化最大比例尺和最小比例尺
   * @private
   * */
  _initScaleLimit() {
    if (this.minScale) {
      const result = this._getScaleZoom(this.minScale)
      if (result.zoom) {
        this.maxZoom = result.zoom
        this._innerView.setMaxZoom(this.maxZoom)
      }
    }
    if (this.maxScale) {
      const result = this._getScaleZoom(this.maxScale)
      if (result.zoom) {
        this.minZoom = result.zoom
        this._innerView.setMinZoom(this.minZoom)
      }
    }
    if (
      !this._scale ||
      this._scale <= this.minScale ||
      this._scale >= this.minScale
    ) {
      this._scale = this.maxScale || this.minScale
      this.setScale(this._scale)
    }
  }

  /**
   * 初始化弹窗
   * @private
   * */
  _initPopup() {
    this.popup = new Popup({ view: this })
  }

  /**
   * <a id='hitTest'></a>
   * @description 穿透检测,图元拾取。目前支持graphic类型拾取结果,支持图层类型GraphicLayer,FeatureLayer。
   * @param {Object} screenPoint  屏幕像素坐标点,例如{ x: 900, y: 500 }
   * @return {Array} 图元检测结果
   * @example <caption><h7>根据基础图层对象或者图层id查询并返回实际图层</h7></caption>
   * // ES6引入方式
   * import { MapView } from "@mapgis/webclient-leaflet-plugin";
   * import { Map, Point, Polygon, MultiPolygon ,Extent, GraphicsLayer, Feature, Circle, IGSFeatureLayer, IGSTileLayer } from "@mapgis/webclient-common";

   * // 初始化图层管理容器
   * const map = new Map();
   * // 初始化地图视图对象
   * this.mapView = new MapView({
   *   // 视图id
   *   viewId: "mapgis-2d-viewer",
   *   // 图层管理容器
   *   map: map,
   * })
   * // 创建一个要素
   * const feature = [
   *   new Feature({
   *     id: '11113',
   *     geometry: new Circle({
   *       center: [113, 35],
   *       radius: 10000,
   *       radiusUnit: 'kilometers',
   *     })
   *   }),
   *   new Feature({
   *     id: '11114',
   *     geometry: new Polygon({
   *       coordinates: [
   *         // 外圈
   *         [
   *           [113.0, 29.0],
   *           [116.0, 29.0],
   *           [116.0, 35.0],
   *           [113.0, 35.0],
   *           [113.0, 29.0]
   *         ]
   *       ]
   *     })
   *   }),
   *   new Feature({
   *     id: '11115',
   *     geometry:new MultiPolygon({
   *       coordinates: [
   *         [
   *           // 外圈
   *           [
   *             [112.0, 28.0],
   *             [115.0, 28.0],
   *             [115.0, 30.0],
   *             [112.0, 30.0],
   *             [112.0, 28.0]
   *           ],
   *           // 第一个内圈
   *           [
   *             [112.2, 28.2],
   *             [112.2, 29.8],
   *             [114.8, 29.8],
   *             [114.8, 28.2],
   *             [112.2, 28.2]
   *           ]
   *         ]
   *       ]
   *     })
   *   })
   * ]
   * // 初始化几何图层
   * const graphicsLayer = new GraphicsLayer({
   *   graphics:feature
   * })
   * map.add(this.graphicsLayer)
   * const result = this.mapView.hitTest({x:1100,y:600})
   * */
  hitTest(screenPoint) {
    let hitTestResult = []
    if (this.preferCanvas) {
      hitTestResult = this._hitTestCanvas(screenPoint)
    } else {
      hitTestResult = this._hitTestSVG(screenPoint)
    }
    return hitTestResult
  }

  /**
   * SVG渲染方式下图元拾取
   * @private
   * @param {Object} screenPoint  屏幕像素坐标点,例如{ x: 900, y: 500 }
   * @return {Array} SVG渲染方式下图元检测结果
   * */
  _hitTestSVG(screenPoint) {
    const self = this
    const pickedInnerLayer = []
    const clientRect = this._innerView._container.getBoundingClientRect()
    const elements = document.elementsFromPoint(
      clientRect.x + screenPoint.x,
      clientRect.y + screenPoint.y
    )
    elements.forEach((el) => {
      if (el.classList.contains('leaflet-interactive') && el._leaflet_id) {
        pickedInnerLayer.push(self._innerView._targets[el._leaflet_id])
      }
    })
    const hitTestResult = []
    pickedInnerLayer.forEach((innerLayer) => {
      const layer = this.getLayer(innerLayer.commonLayerId)
      let features = null
      if (layer.type === LayerType.igsFeature) {
        const layerView = this._getLayerView(layer)
        features = layerView._featureSetCache.filter((item) => {
          if (item.id === innerLayer.commonFeatureId) {
            return item
          }
        })
      } else if (layer.type === LayerType.graphics) {
        features = layer.graphics.filter((item) => {
          if (item.id === innerLayer.commonFeatureId) {
            return item
          }
        })
      }
      const feature =
        features && features.items.length > 0 ? features.items[0] : null
      const graphicHit = {
        graphic: feature,
        layer,
        mapPoint: this.toMap(screenPoint),
        type: 'graphic'
      }
      hitTestResult.push(graphicHit)
    })
    return hitTestResult
  }

  /**
   * Canvas渲染方式下图元拾取
   * @private
   * @param {Object} screenPoint  屏幕像素坐标点,例如{ x: 900, y: 500 }
   * @return {Array} Canvas渲染方式下图元检测结果
   * */
  _hitTestCanvas(screenPoint) {
    const pickedInnerLayer = []
    const renderer = this._innerView._renderer
    const e = { clientX: screenPoint.x, clientY: screenPoint.y }
    const point = renderer._map.mouseEventToContainerPoint(e)
    for (let order = renderer._dragFirst; order; order = order.next) {
      const layer = order.layer
      if (layer.options.interactive && layer._containsPoint(point)) {
        if (
          !(e.type === 'click' || e.type === 'preclick') ||
          !renderer._map._draggableMoved(layer)
        ) {
          if (layer.feature) {
            pickedInnerLayer.push(layer)
          }
        }
      }
    }
    const hitTestResult = []
    pickedInnerLayer.forEach((innerLayer) => {
      const layer = this.getLayer(innerLayer.commonLayerId)
      let features = null
      if (layer.type === 'igs-feature') {
        const layerView = this.getLayerView(layer)
        features = layerView._featureSetCache.filter((item) => {
          if (item.id === innerLayer.commonFeatureId) {
            return item
          }
        })
      } else {
        features = layer.graphics.filter((item) => {
          if (item.id === innerLayer.commonFeatureId) {
            return item
          }
        })
      }
      const feature = features.items.length > 0 ? features.items[0] : null
      const graphicHit = {
        graphic: feature,
        layer,
        mapPoint: this.toMap(screenPoint),
        type: 'graphic'
      }
      hitTestResult.push(graphicHit)
    })
    return hitTestResult
  }

  /**
   * 初始化地图视图比例尺
   * @private
   * */
  _initScale() {
    if (this._scale) {
      if (this.minScale && this._scale > this.minScale) {
        this.setScale()
      }
      if (this.maxScale && this._scale < this.maxScale) {
        this.setScale()
      }
    }
  }

  _getScaleZoom(value) {
    let zoom = undefined
    let scale = undefined
    if (this.crs && this.crs.options && this.crs.options.lods) {
      for (let i = 0; i < this.crs.options.lods.length; i++) {
        const item = this.crs.options.lods[i]
        const lastItem = this.crs.options.lods[i - 1]
        if (i === 0) {
          if (value >= item.scale) {
            zoom = item.level
            scale = item.scale
            break
          }
        } else if (i === this.crs.options.lods.length - 1) {
          if (value <= item.scale) {
            zoom = item.level
            scale = item.scale
            break
          }
        } else if (value >= item.scale && value <= lastItem.scale) {
          zoom = item.level
          scale = item.scale
          break
        }
      }
    }
    return { zoom, scale }
  }

  /**
   * 地图比例尺转视野范围
   * @private
   * @param {Number}  scale 比例尺
   * @return {Extent}  视野范围
   * */
  _getScaleExtent(scale) {
    const extent = this._getScaleDefaultExtent(scale)
    return extent
  }

  /**
   * 地图比例尺转默认坐标系的视野范围
   * @private
   * @param {Number}  scale 比例尺
   * @return {Extent}  视野范围
   * */
  _getScaleDefaultExtent(scale) {
    const ppi = 96 // 1 inch=2.54 厘米(cm)
    const size = this.getSize()
    const lengthX = (size.x / ppi) * 0.0254 * scale // 米
    const lengthY = (size.y / ppi) * 0.0254 * scale
    const length = Math.sqrt(lengthX * lengthX + lengthY * lengthY)
    const absAngle = (180 / Math.PI) * Math.atan2(lengthX, lengthY)

    const center = this._center.coordinates
    const getTargetPoint = (point, distance, bearing) => {
      point = T.point(point)
      const options = { units: 'kilometers' }
      const destination = T.destination(point, distance, bearing, options)
      return destination.geometry.coordinates
    }
    const leftTopPoint = getTargetPoint(center, length / 2000, -absAngle)
    const rightBottomPoint = getTargetPoint(
      center,
      length / 2000,
      180 - absAngle
    )
    const extent = new Extent({
      xmin: leftTopPoint[0],
      xmax: rightBottomPoint[0],
      ymin: rightBottomPoint[1],
      ymax: leftTopPoint[1]
    })
    return extent
  }

  /**
   * <a id='getScale'></a>
   * 获取当前比例尺
   * @return {Number}  比例尺 实际10000米:地图1米
   * */
  getScale0() {
    const ppi = 96 // 1 inch=2.54 厘米(cm)
    const size = this.getSize()
    const screenX = (size.x / ppi) * 0.0254
    const screenY = (size.y / ppi) * 0.0254
    const length = Math.sqrt(screenX * screenX + screenY * screenY)
    const latLngBounds = this._innerView.getBounds()
    const southWestPoint = T.point([
      latLngBounds._southWest.lng,
      latLngBounds._southWest.lat
    ])
    const northEastPoint = T.point([
      latLngBounds._northEast.lng,
      latLngBounds._northEast.lat
    ])
    const options = { units: 'kilometers' }
    const distance = T.distance(southWestPoint, northEastPoint, options)
    const scale = (distance * 1000) / length
    this._scale = scale
    return scale
  }

  getScale() {
    let scale = 0
    if (this.crs && this.crs.options && this.crs.options.lods) {
      if (!isNull(this._zoom)) {
        const lod = this.crs.options.lods[this._zoom]
        scale = lod.scale
      }
    }
    this._scale = scale
    return scale
  }

  /**
   * 通过视野范围参数 获取当前比例尺
   * @param {Extent}  extent 比例尺
   * @return {Number}  比例尺 实际10000米:地图1米
   * @private
   * */
  _getScaleByExtent(extent) {
    const ppi = 96 // 1 inch=2.54 厘米(cm)
    const size = this.getSize()
    const screenX = (size.x / ppi) * 0.0254
    const screenY = (size.y / ppi) * 0.0254
    const length = Math.sqrt(screenX * screenX + screenY * screenY)
    const latLngBounds = this._getLatLngFromExtent(extent)
    const southWestPoint = T.point([
      latLngBounds.southWestLatlng.lng,
      latLngBounds.southWestLatlng.lat
    ])
    const northEastPoint = T.point([
      latLngBounds.northEastLatlng.lng,
      latLngBounds.northEastLatlng.lat
    ])
    const options = { units: 'kilometers' }
    const distance = T.distance(southWestPoint, northEastPoint, options)
    const scale = (distance * 1000) / length
    this._scale = scale
    return scale
  }

  /**
   * <a id='setScale'></a>
   * 设置当前比例尺
   * @private
   * @param {Number} value 比例尺 实际10000米:地图1米
   * */
  setScale(value) {
    if (!this._innerView) return
    const result = this._getScaleZoom(value)
    // 视图定位
    if (result.zoom) {
      this.flyTo({ center: this._center, zoom: result.zoom + 1 })
    }
  }

  /**
   * 获取视图范围的比例尺
   * @private
   * @param {Extent} extent 范围对象
   * @return {Object} 比例尺
   * */
  getExtentScale(extent) {
    const ppi = 96 // 1 inch=2.54 厘米(cm)
    const size = this.getSize()
    const screenX = (size.x / ppi) * 0.0254
    const screenY = (size.y / ppi) * 0.0254
    const length = Math.sqrt(screenX * screenX + screenY * screenY)
    const leftTopPoint = T.point([extent.xmin, extent.ymax])
    const rightBottomPoint = T.point([extent.xmax, extent.ymin])
    const options = { units: 'kilometers' }
    const distance = T.distance(leftTopPoint, rightBottomPoint, options)
    const scale = (distance * 1000) / length
    this._scale = scale
    return scale
  }

  getResolutionByScale(scale) {
    const ppi = 96
    this._resolution = (0.0254 / ppi) * scale
    return this._resolution
  }

  getResolution() {
    const size = this.getSize()
    const length = Math.sqrt(size.x * size.x + size.y * size.y)
    const latLngBounds = this._innerView.getBounds()
    const southWestPoint = T.point([
      latLngBounds._southWest.lng,
      latLngBounds._southWest.lat
    ])
    const northEastPoint = T.point([
      latLngBounds._northEast.lng,
      latLngBounds._northEast.lat
    ])
    const options = { units: 'kilometers' }
    const distance = T.distance(southWestPoint, northEastPoint, options)
    this._resolution = (distance * 1000) / length
    return this._resolution
  }
}

/**
 * 通过一个配置生成一个场景视图对象
 * @param {Object} json 场景视图配置
 * @return {MapView}
 * */
MapView.fromJSON = function (json) {
  json = defaultValue(json, {})

  return new MapView(json)
}

Object.defineProperties(MapView.prototype, {
  scale: {
    get() {
      return this._scale
    },
    set(value) {
      this.setScale(value)
    }
  },
  zoom: {
    get() {
      return this._zoom
    },
    set(value) {
      this.setZoom(value)
    }
  },
  extent: {
    get() {
      return this._extent
    },
    set(value) {
      this.setExtent(value)
    }
  },
  center: {
    get() {
      let center = this._center
      if (this.crs) {
        const spatialReference = new SpatialReference(this.crs.code)
        if (!spatialReference.isGeographic) {
          center = Projection.project(this._center, spatialReference)
        }
      }
      return center
    },
    set(value) {
      this.setCenter(value)
    }
  },
  spatialReference: {
    get() {
      return this._spatialReference
    },
    set(value) {
      this._spatialReference = value
    }
  },
  rotation: {
    get() {
      return this._rotation
    },
    set(value) {
      this._rotation = value
      if (this._innerView) {
        this._innerView.setBearing(value)
      }
    }
  }
})
Zondy.PluginVersion = '16.5.2'
Zondy.MapView = MapView
export default MapView
构造函数
成员变量
方法
事件