src/API.js
import $ from 'jquery'
import { get as getProj, transform } from 'ol/proj'
import BaseObject from 'ol/Object'
import Collection from 'ol/Collection'
import Draw from 'ol/interaction/Draw'
import Modify from 'ol/interaction/Modify'
import { boundingExtent } from 'ol/extent'
import WKT from 'ol/format/WKT'
import { cssClasses, keyCodes } from './globals'
import { Debug } from './Debug'
import { FeatureInteraction } from './interactions/FeatureInteraction'
/**
* @typedef {object} APIMapInteraction
* @property {function} cancel ends the interaction. The result promise will not resolve.
* @property {function} end ends the interaction properly. The result promise will resolve if possible.
* @property {Promise} result a promise that represents the value of the interaction.
*/
// NOTE:
// Access to a source factory would be nice
/**
* @typedef {object} APIOptions
* @property {StyleLike} [drawStyle='#drawStyle']
*/
export class API extends BaseObject {
/**
* @param {G4UMap} map
* @param {object} options
*/
constructor (map, options = {}) {
super()
/**
* @type {boolean}
* @private
*/
this.featureManipulationActive_ = false
/**
* @type {StyleLike}
* @private
*/
this.drawStyle_ = options.drawStyle || '#drawStyle'
/**
* @type {ol.format.WKT}
* @private
*/
this.wktParser_ = new WKT()
/**
* @type {G4UMap}
* @private
*/
this.map_ = map
/**
* @type {?ol.interaction.Interaction}
* @private
*/
this.featureManipulationInteraction_ = null
this.map_.once('ready', () => {
this.layerConfigurator_ = this.map_.get('configurator').layerConfigurator_
})
$(this.map_.getViewport()).on('keydown', this.onKeyDown_.bind(this))
}
// ////////////// FEATURE MANIPULATION ////////////////
endFeatureManipulationInternal_ () {
if (this.featureManipulationInteraction_) {
this.featureManipulationInteraction_.setActive(false)
this.map_.removeInteraction(this.featureManipulationInteraction_)
}
$(this.map_.getViewport()).removeClass(cssClasses.crosshair)
$(this.map_.getViewport()).removeClass(cssClasses.arrow)
// and any other cursor changes
this.featureManipulationActive_ = false
this.dispatchEvent('endManipulation')
}
/**
* cancel the current feature manipulation
*/
cancelFeatureManipulation () {
if (this.featureManipulationActive_) {
this.endFeatureManipulationInternal_()
}
}
/**
* draw a feature
* @param {object} [options={}]
* @param {StyleLike} [options.style]
* @param {string} [options.type='Point'] possible values are: 'Point', 'LineString', 'Polygon', 'MultiPoint',
* 'MultiLineString', 'MultiPolygon' or 'Circle'
* @returns {APIMapInteraction}
*/
drawFeature (options = {}) {
if (this.featureManipulationActive_) {
this.endFeatureManipulationInternal_()
}
this.featureManipulationActive_ = true
let collection = new Collection()
let styleConf = (options.style || this.drawStyle_) || {}
let style = this.map_.get('styling').getStyle(styleConf)
this.map_.get('styling').manageFeatureCollection(collection)
this.featureManipulationInteraction_ = new Draw({
features: collection,
type: options.type || 'Point',
style: style
})
this.map_.addSupersedingInteraction('singleclick dblclick pointermove', this.featureManipulationInteraction_)
$(this.map_.getViewport()).addClass(cssClasses.crosshair)
return {
cancel: () => {
this.endFeatureManipulationInternal_()
},
end: () => {
this.featureManipulationInteraction_.finishDrawing()
this.endFeatureManipulationInternal_()
},
result: new Promise(resolve => {
this.featureManipulationInteraction_.on('drawend', e => {
resolve(e.feature)
this.endFeatureManipulationInternal_()
})
})
}
}
fitRectangle (coordinates, opt = {}) {
if (!opt.hasOwnProperty('srId')) {
opt.srId = 'EPSG:4326'
}
if (!opt.hasOwnProperty('constrainResolution')) {
opt.constrainResolution = false
}
if (!opt.hasOwnProperty('padding')) {
opt.padding = [0, 0, 0, 0]
}
if (getProj(opt.srId)) {
this.map_.getView().fit(
boundingExtent(
[
transform(
[parseFloat(coordinates[0][0]), parseFloat(coordinates[0][1])],
opt.srId,
this.map_.get('mapProjection').getCode()
),
transform(
[parseFloat(coordinates[1][0]), parseFloat(coordinates[1][1])],
opt.srId,
this.map_.get('mapProjection').getCode()
)
]
),
opt
)
} else {
Debug.error(`Unknown Projection '${opt.srId}'`)
}
}
setVisibleBaseLayer (id) {
this.map_.get('baseLayers').recursiveForEach((layer) => {
layer.setVisible(layer.get('id') === id)
})
}
onKeyDown_ (e) {
if (this.featureManipulationActive_ && e.which === keyCodes.ESCAPE) {
this.endFeatureManipulationInternal_()
}
}
/**
* Select a feature with a single click
* @returns {APIMapInteraction}
*/
selectFeature () {
if (this.featureManipulationActive_) {
this.endFeatureManipulationInternal_(null)
}
this.featureManipulationActive_ = true
this.featureManipulationInteraction_ = new FeatureInteraction({
type: 'singleclick'
})
this.map_.addSupersedingInteraction('singleclick', this.featureManipulationInteraction_)
$(this.map_.getViewport()).addClass(cssClasses.arrow)
return {
cancel: () => {
this.endFeatureManipulationInternal_()
},
end: () => {
this.endFeatureManipulationInternal_()
},
result: new Promise((resolve) => {
this.featureManipulationInteraction_.on('interaction', e => {
if (e.interacted.length) {
resolve(e.interacted[0].feature, e.interacted[0].layer)
this.endFeatureManipulationInternal_()
}
})
})
}
}
/**
* Modify a given Feature. The end function needs to be called to indicate that a modifying process is completed.
* @param {ol.Collection<ol.Feature>|ol.Feature[]|ol.Feature} feature
* @param {Object} options
* @param {StyleLike} [options.style]
* @returns {APIMapInteraction}
*/
modifyFeature (feature, options = {}) {
options.features = new Collection([feature])
if (this.featureManipulationActive_) {
this.endFeatureManipulationInternal_(false)
}
this.featureManipulationActive_ = true
if (options.style) {
options.style = this.map_.get('styling').getStyle(options.style)
}
this.featureManipulationInteraction_ = new Modify(options)
this.map_.addSupersedingInteraction('singleclick dblclick pointermove', this.featureManipulationInteraction_)
$(this.map_.getViewport()).addClass(cssClasses.crosshair)
let ended = false
return {
cancel: () => {
this.endFeatureManipulationInternal_()
},
end: () => {
ended = true
this.endFeatureManipulationInternal_()
},
result: new Promise(resolve => {
this.featureManipulationInteraction_.on('modifyend', () => {
if (ended) {
resolve(feature)
}
})
this.featureManipulationInteraction_.once('change:active', () => {
if (ended) {
resolve(feature)
}
})
})
}
}
/**
* This function creates a layer from the given layerOptions and adds it the map
* @param {g4uLayerOptions} layerOptions
* @returns {VectorLayer}
*/
addFeatureLayer (layerOptions) {
return this.layerConfigurator_.getFactory()
.addLayer(this.map_.get('featureLayers'), layerOptions, 'featureLayer', true)
}
/**
* This function creates a layer from the given layerOptions and adds it as a fixedFeatureLayer to the map
* @param {g4uLayerOptions} layerOptions
* @returns {VectorLayer}
*/
addFixedFeatureLayer (layerOptions) {
return this.layerConfigurator_.getFactory()
.addLayer(this.map_.get('fixedFeatureLayers'), layerOptions, 'featureLayer', true)
}
/**
* This function creates a base layer from the given layerOptions and adds it to the map
* @param {g4uLayerOptions} layerOptions
* @returns {ol.layer.Base}
*/
addBaseLayer (layerOptions) {
return this.layerConfigurator_.getFactory()
.addLayer(this.map_.get('baseLayers'), layerOptions, 'baseLayer', true)
}
/**
* This function creates a layer from the given layerOptions, adds it as a VectorLayere and returns a promise which
* is resolved as soon as the layer is loaded fully.
* @param {g4uLayerOptions} layerOptions
* @returns {Promise.<VectorLayer>}
*/
loadLayerFromServer (layerOptions) {
layerOptions = layerOptions || {}
layerOptions.visible = true
layerOptions.source = layerOptions.source || {}
let promise = new Promise((resolve, reject) => {
let layer = this.addFeatureLayer(layerOptions)
let source = layer.getSource()
let loadEndHandler = () => {
source.un('vectorloadend', loadErrorHandler)
resolve(layer)
}
let loadErrorHandler = () => {
source.un('vectorloaderror', loadEndHandler)
reject(new Error('vector load error'))
}
source.once('vectorloadend', loadEndHandler)
source.once('vectorloaderror', loadErrorHandler)
})
return promise
}
/**
* Creates a Feature from the given config
* @param {FeatureConfig} config
* @returns {ol.Feature}
*/
createFeature (config) {
return this.layerConfigurator_.getFactory().createFeature(config)
}
/**
* Removes a layer from the map
* @param {ol.layer.Base} layer
*/
removeLayer (layer) {
this.map_.getLayerGroup().removeLayer(layer)
}
}