Home Reference Source

src/configurators/LayerFactory.js

import $ from 'jquery'
import Observable from 'ol/Observable'

import { containsExtent } from 'ol/extent'
import { all, tile } from 'ol/loadingstrategy'
import VectorSource from 'ol/source/Vector'
import XYZ from 'ol/source/XYZ'
import OSM from 'ol/source/OSM'
import Stamen from 'ol/source/Stamen'
import BingMaps from 'ol/source/BingMaps'
import { createXYZ } from 'ol/tilegrid'
import TileGrid from 'ol/tilegrid/TileGrid'
import WMTSTileGrid from 'ol/tilegrid/WMTS'
import WKT from 'ol/format/WKT'
import Feature from 'ol/Feature'
import WMTSCapabilities from 'ol/format/WMTSCapabilities'
import WMTS, { optionsFromCapabilities } from 'ol/source/WMTS'
import ImageCanvas from 'ol/source/ImageCanvas'

import { ImageLayer } from '../layers/ImageLayer'
import { EmptyImageLayer } from '../layers/EmptyImageLayer'
import { TileLayer } from '../layers/TileLayer'
import { VectorLayer, VectorImageLayer } from '../layers/VectorLayer'
import { ArcGISRESTFeatureSource } from '../sources/ArcGISRESTFeatureSource'
import { SourceServerVector } from '../sources/SourceServerVector'
import { copyDeep, mergeDeep, take } from '../utilitiesObject'
import { asObject, checkFor } from '../utilities'

import { Debug } from '../Debug'
import { ImageWMSSource, TileWMSSource } from '../sources/ImageWMSSource'
import { SilentGroupLayer } from '../layers/SilentGroupLayer'
import { URL } from '../URLHelper'
import { ClusterSource } from '../sources/ClusterSource'
import { WMTSSource } from '../sources/WMTSSource'

/**
 * @enum {string} LayerType
 */
export const LayerType = {
  GROUP: 'Group',
  GEOJSON: 'GeoJSON',
  KML: 'KML',
  WMS: 'WMS',
  TILEWMS: 'TileWMS',
  WMTS: 'WMTS',
  OSM: 'OSM',
  STAMEN: 'Stamen',
  INTERN: 'Intern',
  EMPTY: 'Empty',
  XYZ: 'XYZ',
  BING: 'Bing',
  ARCGISRESTFEATURE: 'ArcGISRESTFeature'
}

/**
 * @property {number} [tileSize=512] If set the tile loading strategy will use tiles of this size
 * This class constructs a layer according to the given {{LayerOptions}}
 */
export class LayerFactory extends Observable {
  /**
   * @param {G4UMap} map
   */
  constructor (map) {
    super()
    /**
     * @type {G4UMap}
     * @private
     */
    this.map_ = map
  }

  /**
   * Creates a layer or a collection of layers from the config
   * @param {g4uLayerOptions} optionsCopy
   * @returns {ol.layer.Layer|ol.Collection.<ol.layer.Layer>}
   */
  createLayer (options) {
    const optionsCopy = copyDeep(options)

    if (!optionsCopy.type) {
      throw new Error(`Layer needs a type. Layer id: ${optionsCopy.id}. Layer title: ${optionsCopy.title}.`)
    }

    const layerType = optionsCopy.type

    // availability
    this.configureLayerAvailability_(optionsCopy)

    // the title/name of the layer
    this.configureLayerTitle_(optionsCopy)

    if (optionsCopy.source) {
      this.configureLayerSourceAttribution_(optionsCopy.source)
    }

    // visibility
    this.configureLayerVisibility_(optionsCopy)

    let layer
    let layerConfigs
    let clusterOptions
    let localised = false
    if (optionsCopy.source) {
      optionsCopy.source.localiser = this.map_.get('localiser')
      localised = optionsCopy.source.localised
    }

    const style = take(optionsCopy, 'style')

    switch (layerType) {
      case LayerType.SILENTGROUP:

        layerConfigs = take(optionsCopy, 'layers')

        layer = new SilentGroupLayer(optionsCopy)

        for (const options of layerConfigs) {
          options.visible = true
          layer.getLayers().add(this.createLayer(options))
        }

        break
      // case LayerType.CATEGORY:
      //   layerConfigs = take(optionsCopy, 'layers')
      //
      //   layer = new GroupLayer(optionsCopy)
      //
      //   this.addLayers(layer, layerConfigs, superType, skipIdCheck)
      //
      //   // TODO: realize childrenAvailable
      //   // availability & parent ref
      //   let childrenAvailable = false
      //   layer.getLayers().forEach(childLayer => {
      //     childLayer.set('category', layer)
      //     childrenAvailable = childrenAvailable || childLayer.get('available')
      //   })
      //
      //   let childrenCount = layer.getLayers().getLength()
      //
      //   if (childrenAvailable !== false) {
      //     if ((layer.get('available') !== false) && (childrenCount > 0)) {
      //       // category is shown
      //
      //       if (childrenAvailable === true) {
      //         layer.set('available', true)
      //       }
      //
      //       layer.setVisible(true)
      //
      //       if (superType === SuperType.BASELAYER) {
      //         layer.set('activateChildren', false)
      //       }
      //
      //       return layer
      //     } else if (childrenAvailable === true) {
      //       // only children are shown
      //       return layer.getLayers()
      //     }
      //     // else neither are shown
      //   }
      //   break
      case LayerType.EMPTY:
        layer = new EmptyImageLayer(optionsCopy)
        break
      case LayerType.XYZ:
        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_).finalize()
        optionsCopy.source = new XYZ(optionsCopy.source)

        layer = new TileLayer(optionsCopy)
        break
      case LayerType.OSM:
        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_).finalize()
        optionsCopy.source = new OSM(optionsCopy.source)

        layer = new TileLayer(optionsCopy)
        break
      case LayerType.STAMEN:
        optionsCopy.source = new Stamen(optionsCopy.source)

        layer = new TileLayer(optionsCopy)
        break
      case LayerType.BING:
        optionsCopy.source = new BingMaps(optionsCopy.source)

        layer = new TileLayer(optionsCopy)
        break
      case LayerType.WMS:
        if (optionsCopy.categoryButtons) {
          optionsCopy.source.params.LAYERS = []
        }

        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_) // not finalized

        optionsCopy.source = new ImageWMSSource(optionsCopy.source)
        layer = new ImageLayer(optionsCopy)

        this.map_.asSoonAs('ready:ui', true, () => {
          if (this.map_.get('showWMSFeatureInfo')) {
            this.map_.get('showWMSFeatureInfo').addLayer(layer)
          }
        })

        break
      case LayerType.TILEWMS:
        if (optionsCopy.source.tileSize) {
          optionsCopy.source.tileGrid = createXYZ({ tileSize: optionsCopy.source.tileSize })
          delete optionsCopy.source.tileSize
        } else if (optionsCopy.source.tileGrid) {
          optionsCopy.source.tileGrid = new TileGrid(optionsCopy.source.tileGrid)
        }

        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_) // not finalized

        // // TODO: wms query layer
        // optionsCopy.visible = false
        // optionsCopy.source = new QueryTileWMSSource(optionsCopy.source)
        // layer = new TileLayer(optionsCopy)

        optionsCopy.source = new TileWMSSource(optionsCopy.source)
        layer = new TileLayer(optionsCopy)

        this.map_.asSoonAs('ready:ui', true, () => {
          if (this.map_.get('showWMSFeatureInfo')) {
            this.map_.get('showWMSFeatureInfo').addLayer(layer)
          }
        })

        break
      case LayerType.WMTS: {
        const sourceOptions = take(optionsCopy, 'source')

        if (!sourceOptions.autoConfig) {
          if (!sourceOptions.tileGrid) {
            throw new Error('You have to provide either a tileGrid or use autoConfig for WMTS Source')
          }

          sourceOptions.tileGrid = new WMTSTileGrid(sourceOptions.tileGrid)

          sourceOptions.url = URL.extractFromConfig(sourceOptions, 'url', undefined, this.map_) // not finalized

          optionsCopy.source = new WMTSSource(sourceOptions)
        }
        // url gets extracted in setWMTSSourceFromCapabilities

        layer = new TileLayer(optionsCopy)

        if (sourceOptions.autoConfig) {
          this.setWMTSSourceFromCapabilities(layer, sourceOptions)
        }

        break
      }
      case LayerType.GEOJSON:
        this.configureLayerSourceLoadingStrategy_(optionsCopy.source)
        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_) // not finalized

        optionsCopy.source.defaultStyle = this.map_.get('styling').getStyle(style || '#defaultStyle')

        optionsCopy.source.type = 'GeoJSON'

        optionsCopy.source.bboxProjection = optionsCopy.source.bboxProjection || this.map_.get('interfaceProjection')

        optionsCopy.source.styling = this.map_.get('styling')

        clusterOptions = take(optionsCopy.source, 'cluster')

        optionsCopy.source = new SourceServerVector(optionsCopy.source)

        if (clusterOptions) {
          clusterOptions = asObject(clusterOptions)
          optionsCopy.source = new ClusterSource(optionsCopy.source, clusterOptions)
        }
        if (options.renderMode !== 'image') {
          layer = new VectorLayer(optionsCopy)
        } else {
          layer = new VectorImageLayer(optionsCopy)
        }

        break
      case LayerType.KML:
        this.configureLayerSourceLoadingStrategy_(optionsCopy.source)
        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_) // not finalized

        optionsCopy.source.defaultStyle = this.map_.get('styling').getStyle(style || '#defaultStyle')

        optionsCopy.source.type = 'KML'

        optionsCopy.source.bboxProjection = optionsCopy.source.bboxProjection || this.map_.get('interfaceProjection')

        optionsCopy.source.styling = this.map_.get('styling')

        clusterOptions = take(optionsCopy.source, 'cluster')

        optionsCopy.source = new SourceServerVector(optionsCopy.source)

        if (clusterOptions) {
          optionsCopy.source = new ClusterSource(optionsCopy.source, asObject(clusterOptions))
        }

        if (options.renderMode !== 'image') {
          layer = new VectorLayer(optionsCopy)
        } else {
          layer = new VectorImageLayer(optionsCopy)
        }
        break
      case LayerType.ARCGISRESTFEATURE:
        this.configureLayerSourceLoadingStrategy_(optionsCopy.source)
        optionsCopy.source.url = URL.extractFromConfig(optionsCopy.source, 'url', undefined, this.map_) // not finalized

        optionsCopy.source = new ArcGISRESTFeatureSource(optionsCopy.source)

        layer = new VectorLayer(optionsCopy)
        break
      case LayerType.INTERN:

        if (optionsCopy.source && optionsCopy.source.hasOwnProperty('features')) {
          for (let i = 0; i < optionsCopy.source.features.length; i++) {
            optionsCopy.source.features[i] = this.createFeature(optionsCopy.source.features[i])
          }
        }

        optionsCopy.source = new VectorSource(optionsCopy.source)

        layer = new VectorLayer(optionsCopy)
        break
    }

    for (const module of this.map_.getModules()) {
      if (layer) {
        break
      }
      layer = module.createLayer(optionsCopy)
    }

    if (!layer) {
      throw new Error(`layer with type '${optionsCopy.type}' could not be created.`)
    }

    // styling
    if (layer.setStyle) {
      layer.setStyle(this.map_.get('styling').getStyle(style))
      this.map_.get('styling').manageLayer(layer)
    }

    // if layer is being localised, refresh on language change
    if (localised) {
      this.map_.get('localiser').on('change:language', () => layer.getSource().refresh())
    }

    this.dispatchEvent({
      type: 'created:layer',
      layer: layer,
      options: options
    })

    return layer
  }

  /**
   * @param {g4uLayerOptions} config
   * @returns {g4uLayerOptions}
   * @private
   */
  configureLayerAvailability_ (config) {
    // if available is set to false explicitely, the layer won't be available
    config.available = (config.available !== false)

    if (!config.alwaysVisible) {
      config.available = config.hasOwnProperty('availableMobile') ? config.availableMobile : config.available
      if (this.map_.get('ignoreLayerAvailability')) {
        config.available = true
      }
    }
    return config
  }

  /**
   * @param {SourceConfig} sourceConfig
   * @returns {SourceConfig}
   * @private
   */
  configureLayerSourceAttribution_ (sourceConfig) {
    if (checkFor(sourceConfig, 'attribution')) {
      sourceConfig.attributions = [this.map_.get('localiser').selectL10N(sourceConfig.attribution)]
    }
    return sourceConfig
  }

  /**
   * @param {SourceConfig} sourceConfig
   * @returns {SourceConfig}
   * @private
   */
  configureLayerSourceLoadingStrategy_ (sourceConfig) {
    const loadingStrategy = sourceConfig.hasOwnProperty('loadingStrategy')
      ? sourceConfig.loadingStrategy
      : this.map_.get('loadingStrategy')

    if (loadingStrategy === 'BBOX') {
      const bboxRatio = sourceConfig.bboxRatio || 1

      if (bboxRatio < 1) {
        throw new Error('The bboxRatio should not be smaller than 1')
      }

      let lastScaledExtent = [0, 0, 0, 0]

      sourceConfig.strategy = (extent) => {
        if (containsExtent(lastScaledExtent, extent)) {
          return [extent]
        } else {
          const deltaX = ((extent[2] - extent[0]) / 2) * (bboxRatio - 1)
          const deltaY = ((extent[3] - extent[1]) / 2) * (bboxRatio - 1)

          lastScaledExtent = [
            extent[0] - deltaX,
            extent[1] - deltaY,
            extent[2] + deltaX,
            extent[3] + deltaY
          ]

          return [lastScaledExtent]
        }
      }
    } else if (loadingStrategy === 'TILE') {
      sourceConfig.strategy = tile(createXYZ({
        tileSize: sourceConfig.tileSize || 512
      }))
    } else {
      sourceConfig.strategy = all
    }

    sourceConfig.loadingStrategyType = loadingStrategy

    return sourceConfig
  }

  /**
   * @param {g4uLayerOptions} config
   * @returns {g4uLayerOptions}
   * @private
   */
  configureLayerTitle_ (config) {
    if (!config.hasOwnProperty('title')) {
      config.title = 'No title given'
    } else {
      config.title = this.map_.get('localiser').selectL10N(config.title)
    }
    return config
  }

  /**
   * @param {g4uLayerOptions} config
   * @returns {g4uLayerOptions}
   * @private
   */
  configureLayerVisibility_ (config) {
    if (config.alwaysVisible === true) {
      config.visible = true
    } else {
      config.visible = (config.visible === true)
    }

    return config
  }

  /**
   * @param {FeatureConfig} featureConf
   * @returns {ol.Feature}
   */
  createFeature (featureConf) {
    /**
     * @type {FeatureConfig}
     */
    const featureConfCopy = copyDeep(featureConf)

    const id = take(featureConfCopy, 'id')

    const style = take(featureConfCopy, 'style')

    const format = new WKT()
    const wkt = take(featureConfCopy, 'geometryWKT') || take(featureConfCopy, 'geographyWKT')
    featureConfCopy.geometry = format.readGeometry(wkt)
      .transform(this.map_.get('interfaceProjection'), this.map_.get('mapProjection'))

    const feature = new Feature(featureConfCopy)

    if (style) {
      this.map_.get('styling').styleFeature(feature, style)
    }

    if (id) {
      feature.setId(id)
    }

    return feature
  }

  setWMTSSourceFromCapabilities (layer, sourceOptions) {
    const url = URL.extractFromConfig(sourceOptions, 'url', undefined, this.map_)
    const capUrl = url.clone().addParam('Service=WMTS').addParam('Request=GetCapabilities')
    $.ajax({
      url: capUrl.finalize(),
      dataType: 'text xml',
      crossDomain: true,
      success: data => {
        const wmtsCap = (new WMTSCapabilities()).read(data)
        const capOptions = optionsFromCapabilities(wmtsCap, take(sourceOptions, 'config'))
        if (capOptions === null) {
          Debug.error(`wmts layer not found or not set for layer with id "${layer.get('id')}"`)
        } else {
          capOptions.urls = capOptions.urls.map(newUrl => {
            const u = url.clone()
            u.url = newUrl
            return u.finalize()
          })
          sourceOptions = mergeDeep(sourceOptions, capOptions)
          layer.setSource(new WMTS(sourceOptions))
        }
      }
    })
    layer.setSource(new ImageCanvas({
      state: 'ready',
      canvasFunction: () => {} // not loading any canvas
    }))
  }
}