OpenLayer基于vue的封装使用

前言

公司项目使用了openlayer作为2d平面地图来使用,之前没有接触过,开一篇文章记录一下。顺便捋一下代码里面封装的结构。

基本结构 

openlayer使用的版本是"^6.4.3",引入了mapbox的样式,"ol-mapbox-style": "^8.2.0"。地图的初始化专门封装了一个class类,用于初始化地图使用。

import Object from 'ol/Object'
import View from 'ol/View'
import Map from 'ol/Map'

class EMap extends Object {
  constructor (options) {
    super(options)
    this.options = assignObj({}, options)
    this._view = undefined
    this._baseLayers = []
    this._map = undefined
    this.vectorLayers = []
    this.rasterLayers = []
    this.controls = []
    this._mapClickFunc = options.mapclickFunction
    this._mapEvtBus = options.mapEvtBus

    this._interactionsState = {}
    this._initMap()
  }
}

assignObj方法是Object.assign方法,但是刚好ol自己有一个Object类,避免冲突就需要更改一下这个方法名了。

主要结构有这几种:map地图,view视图,layer图层,controls控制器,mapClickFunc地图相关的点击事件,mapEvtBus地图事件总线。

_initMap()方法用来初始化地图。方法代码内容如下:

  _initMap () {
    this._view = this._createView()
    this._baseLayers = this._createBaseLayer()
    this._map = this._createMap()
    this._initMapEvt()
  }

_createView 

_createView()方法用来初始化view视图。方法代码内容如下:

import {get as getProject} from 'ol/proj' 
 _createView () {
    let viewOptions = assignObj( this._getDefaultViewOptions(), this.options.view)
    if (!viewOptions.projectionCode) {
      viewOptions.projection = 'EPSG:3857'
    } else {
      viewOptions.projection = `EPSG:${viewOptions.projectionCode}`
    }
    delete viewOptions.projectionCode

    // let projection = getProject(viewOptions.projection)
    // if (!projection) {
    //  projection = getProject('EPSG:4326')
    // }
    // let projectionExtent = projection.getExtent()
    // let width = getWidth(projectionExtent)
    // let resolutions = []
    // for (let z = 0; z < 25; z++) {
    //   resolutions[z] = width / (256 * Math.pow(2, z))
    // }

    // console.log('分辨率1', resolutions)

    // viewOptions.resolutions = resolutions
    let view = new View(viewOptions)
    return view
  }

首先通过_getDefaultViewOptions方法,获取view的一些默认配置,然后将传入的options的配置使用assign方法进行合并。

然后就是判断坐标系编码,这个判断逻辑可以根据需要来更改,ol默认的坐标系就是3857,在官网中有说明。

 注释掉的代码,是对分辨率进行的处理,根据需要可以自行添加进去。

_getDefaultViewOptions()方法存储一些默认配置,比如中心点,坐标系,缩放这种。

_getDefaultViewOptions() {
  let options = {
    projectionCode: '3857',
    center: [120, 69],
    zoom: 5
  }
  return options
}

如果地图的配置项是通过接口获取数据,那默认配置最好和接口返回的数据对应,这样即使接口中有某个数据没法通过校验,就可以使用默认值了。校验方法放在_createView中和默认配置分开,逻辑会清晰点,不会挤在同一个方法里面。

_createBaselayer

_createBaselayer()方法主要是创建底图,底图可能是天地图,mapbox,高德,百度等,因此不同的底图执行的代码逻辑是不一样的,需要加判断分别处理。

  _createBaseLayer () {
    const baseLayerOptions = this.options.baseLayer
    if (!baseLayerOptions.type ) {
      baseLayerOptions.type = 'mapbox'
    }

    if (baseLayerOptions.type === 'tianditu') {
      return this._createTianDiTuLayers(baseLayerOptions)
    } else if (baseLayerOptions.type === 'mapbox') {
      return this._createMapBoxLayers(baseLayerOptions)
    } else {
      return this._createXYZLayer(baseLayerOptions)
    }
  }

以处理天地图_createTianDiTuLayers为例,通过接口请求到的底图参数中有一个baseLayer属性,存储一个对象,除了携带type属性外,还有对应的token信息。

import {createXYZ} from 'ol/tilegrid'
import Tile from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'

_createTianDiTuLayers() {
    const tdtToken = baseLayerOptions.tdtToken
    const baseURL = 'http://t{0-7}.tianditu.gov.cn/'
    const layerOptions = [
      {
        title: '天地图矢量',
        layerName: 'vec_c',
        attributions: '右下角署名',
        visible: true,
        type: 'vec'
      },
      {
        title: '天地图矢量注记',
        layerName: 'cva_c',
        attributions: '',
        visible: true,
        type: 'vec'
      },
      {
        title: '天地图卫星影像',
        layerName: 'img_c',
        attributions: '右下角署名',
        visible: false,
        type: 'img'
      },
      {
        title: '天地图卫星影像注记',
        layerName: 'cia_c',
        attributions: '',
        visible: false,
        type: 'img'
      },
    ]
}

底图可以是多个图层叠加的,因此baseLayers是一个数组。layerOptions存储了一些天地图的信息,通过visible设置是否启用,一般是矢量图或者图片加上对应的标注。

    var projection = new getProject('EPSG:3857')

    let tilegrid = createXYZ({
      extent: projection.getExtent()
    })

    const layers = layerOptions.map((item) => {
      let layerType = item.layerName.split('_')
      const url = `${baseURL}${item.layerName}/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layerType[0]}&STYLE=default&TILEMATRIXSET=${layerType[1]}&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tdtToken}`
      const attributions = item.attributions === '' ? undefined : `© <a href="http://www.baidu.com" target="_blank">${item.attributions}</a>`
      const layer = new Tile({
        title: item.title,
        source: new XYZ({
          attributions: attributions,
          url: url,
          wrapx: false,
          crossOrigin: 'anonymous',
          projection: projection,
          tileGrid: tilegrid
        }),
        minZoom: 0,
        maxZoom: 20
      })
      layer.setProperties({
        layerType: item.type,
        isBaseLayer: true
      })
      layer.setVisible(item.visible)
      return layer
    })

最主要的内容还是layer,使用ol/layer/Tile设置标题,数据源,最大最小缩放,tileGrid根据坐标系设置范围。openlayer的图层添加后,会在右下角有一个感叹号,里面的内容就是由source的attributions来定义的。crossOrigin是设置canvas的跨域属性。mdn对它有解释,它有三种值可以设置。

是h5的特性支持,和openlayer无关就是了。

为layer设置了两个值,这两个值本身是没有的,用setProperties添加进去。后续可以使用getProperties()来获取这两个值。根据设置好的visible设置layer的可见性。这样关于天地图的底图设置逻辑就完成了。 

_createMap

_createMap()方法创建map地图,添加一些控件,代码中添加了一个比例尺

import ScaleLine from 'ol/control/ScaleLine'
import { defaults } from 'ol/control'

  _createMap () {
    const map = new Map({
      target: this.options.target,
      view: this._view,
      layers: this._baseLayers,
      controls: new defaults({
        attribution: true,
        attributionOptions: {
          tipLabel: '信息'
        },
        zoomOptions: {
          zoomInTipLabel: '放大',
          zoomOutTipLabel: '缩小',
        }
      })
    })
    const scale = new ScaleLine({
      bar: true, 
      text: true, 
      minWidth: 125
    })
    map.addControl(scale)
    return map
  }

 _initMapEvt

_initMapEvt()处理地图的一些控制和交互功能。

  _initMapEvt () {
    this._initMapControl()
    this._initMapClickEvent()
    this._initPointMoveEvent()
  }

 _initMapControl

_initMapControl()方法主要是去除一些容易和后面的操作冲突的事件。

import DoubleClickZoom from 'ol/interaction/DoubleClickZoom'

  _initMapControl () {
    // 移除双击缩放控件(与双击弹属性窗冲突)
    let controls = this._map.getInteractions()
    let dbClickZoomControl = controls.getArray().find((control) => control instanceof DoubleClickZoom)
    if(dbClickZoomControl) {
      this._map.removeInteraction(dbClickZoomControl)
    }

    this._singleClickControl = new Select({
      condition: function (evt) {
        return evt.type === 'singleclick' || evt.type === 'dblclick'
      },
      // style: this._singleClickStyle.bind(this), // 如果需要自定义每个图层的选中样式,请开启这个属性
      layers: function (layer) {
        const layerName = layer.rootLayerName
        return this.findLayer(this.vectorLayers, layerName)
      }.bind(this)
    })

    var selectedFeatures = this._singleClickControl.getFeatures()
    selectedFeatures.on(['add','remove'], (evt) => {
      this.dispatchEvent({
        type: 'selectDataChanged',
        target: selectedFeatures,
        element: evt.element,
        option: evt.type
      })
    })

    if(this._map) {
      this._map.addInteraction(this._singleClickControl)
    }
  }

 使用getInteractions()获取到所有交互,用类型检测出双击事件,然后移除。再加入自定义的singleClickControl,在add和remove的时候触发。

 _initMapClickEvent()

  _initMapClickEvent () {
    this._map.on('click', (evt) => {
      // 单击事件优先选择控件中的单击选中事件
      const features = this._map.getFeaturesAtPixel(evt.pixel)
      if(features.length > 0) {
        features.forEach((ft) => {
          // map上的单击事件和layer的单击事件,取其一,优先map
          if(this._mapClickFunc) {
            this._mapClickFunc({
              data: ft,
              evt: evt
            })
          } else {
            const layerName = ft.get('layerName')
            const eLayer = this.findLayer(this.vectorLayers, layerName)
            if(eLayer) {
              eLayer._singleClick(ft, evt)
            }
          }
        })
      } else {
        if(this._mapClickFunc) {
          this._mapClickFunc({
            data: undefined,
            evt: evt
          })
        }
      }
    })

    this._map.on('dblclick', (evt) => {
      const features = this._map.getFeaturesAtPixel(evt.pixel)
      if (features.length > 0) {
        features.forEach((ft) => {
          const layerName = ft.get('layerName')
          const eLayer = this.findLayer(this.vectorLayers, layerName)
          if(eLayer) {
            eLayer._dbClick(ft, evt)
          }
        })
      }
    })
  }

_initMapClickEvent()主要处理单击和双击事件,后续加入进去的layer图层可以自己定义单击事件。初始化map对象的时候,也可以自己传入mapClickFunc。代码中优先取map的单击事件。

findLayer方法为自定义方法,主要是通过layername拿到对应的layer。

_initPointMoveEvent

  _initPointMoveEvent () {
    this._map.on('pointermove', (evt) => {
      const features = this._map.getFeaturesAtPixel(evt.pixel)
      if(features.length > 0) {
        this._map.getTargetElement().style.cursor = 'pointer'
      } else {
        this._map.getTargetElement().style.cursor = 'auto'
      }
    })
  }

 _initPointMoveEvent()方法,当鼠标移动到某个features上时候,鼠标形状改变。用来告诉用户,鼠标位置存在可以交互的东西。

然后就是一些普通的getter和setter方法。可以按自己喜好多封装一些常用的。

  getOlMap () {
    return this._map
  }

  getView () {
    return this._view
  }

  getMapProjection () {
    return this.getView().getProjection()
  }

  getZoom () {
    if(this._view) {
      return this._view.getZoom()
    }
  }

  getBaseLayers () {
    return this._baseLayers
  }

  setZoom (zoom) {
    if (this._view) {
      this._view.setZoom(zoom)
    }
  }

  setCenter (point) {
    this._view.setCenter(point)
  }

  setView (view) {
    this._map.setView(view)
    this._view = view
  }

  zoomToNext () {
    let zoom = this.getZoom()
    zoom = parseInt(zoom)
    this.setZoom(zoom + 1)
  }

  fit (geom) {
    this._view.fit(geom)
  }

  fitToLayer (eLayer) {
    if(eLayer.getDataExtent) {
      const extent = eLayer.getDataExtent()
      const resolution = this._view.getResolution()
      // 范围缩小一点,要不然碰到地图边界
      extent[0] = extent[0] - 1 * resolution
      extent[1] = extent[1] - 1 * resolution
      extent[2] = extent[2] + 1 * resolution
      extent[3] = extent[3] + 1 * resolution
      if (extent) {
        this.fit(extent)
      }
    }
  }

  zoomToPrevious () {
    let zoom = this.getZoom()
    zoom = parseInt(zoom)
    this.setZoom(zoom - 1)
  }

  getExtent () {
    return this._view.calculateExtent(this._map.getSize())
  }

地图的初始化操作就这么多,接下来就是一些layer图层上面的添加,查找,移除的操作。

import _ from 'lodash'

  addLayer (eLayer) {
    const layer = eLayer.getLayer()
    if (layer) {
      if (eLayer.get('eLayerType') === layerDataType.vector) {
        if (!this.findLayer(this.vectorLayers, eLayer.get('layerName'))) {
          this.vectorLayers.push(eLayer)
          this._map.addLayer(layer)
        } else {
          console.log('layer is exist')
        }
      } else if (eLayer.get('eLayerType') === layerDataType.raster ) {
        if (!this.findLayer(this.rasterLayers, eLayer.get('layerName'))) {
          this.rasterLayers.push(eLayer)
          this._map.addLayer(layer)
        } else {  
          console.log('layer is exist')
        }
      } else {
        console.log('layer is not eMapLayer...')
      }
    }
  }

  findLayer (layerList, layerName) {
    if (layerList) {
      const layer = _.find(layerList, (layer) => {
        return layer.get('layerName') === layerName
      })
      return layer
    }
    return null
  }

  removeLayer (eLayer) {
    const layer = eLayer.getLayer()
    if (layer) {
      if (eLayer.get('eLayerType') === layerDataType.vector) {
        _.remove(this.vectorLayers, (layer) => {
          return layer === eLayer
        })
        this._map.removeLayer(layer)
      } else if(eLayer.get('eLayerType') === layerDataType.raster) {
        _.remove(this.rasterLayers, (layer) => {
          return layer === eLayer
        })
        this._map.removeLayer(layer)
      } else {
        console.log('layer is not eMapLayer...')
      }
    }
  }

  removeLayerByName (layerName) {
    let eLayer = this.findLayer(this.vectorLayers, layerName)
    if (eLayer) {
      this.removeLayer(eLayer)
    } else {
      eLayer = this.findLayer(this.rasterLayers, layerName)
      if (eLayer) {
        this.removeLayer(eLayer)
      }
    }
  }

后面layer图层也会进行一次封装,有一个eLayerType的字符串值,决定是放在哪个图层数组里面。名称不能重复,如果检测到重复名称说明图层已经添加过了,就不会重新添加了。

当存在一些编辑功能的时候,防止冲突,就要停止和恢复一些交互功能。封装两个方法。

  /**
   * 暂停作用域以外的交互控件(默认不暂停)
   * @param {string}} scope 
   */
  pauseInteraction (scope) {
    let interactions = this._map.getInteractions()
    interactions.forEach((itc) => {
      if(!itc.rootName) {
        return
      }
      if(itc.rootName !== scope) {
        let id = itc.ol_uid
        this._interactionsState[id] = itc.getActive()
        itc.setActive(false)
      }
    })
  }

  resumeInteraction () {
    let interactions = this._map.getInteractions()
    interactions.forEach((itc) => {
      if(itc.rootName) {
        let id = itc.ol_uid
        let active = this._interactionsState[id]
        if(active) {
          itc.setActive(active)
        }
      }
    })
  }

单击显示的数据

  showDetail (data, zoom, point, id, geomType) {
    if (this._mapEvtBus) {
      const options = {
        data,
        zoom,
        point,
        id,
        geomType
      }
      this._mapEvtBus.$emit('showDetail', options)
    }
  }

猜你喜欢

转载自blog.csdn.net/GhostPaints/article/details/127257586