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
}))
}
}