src/Styling.js
import $ from 'jquery'
import Circle from 'ol/style/Circle'
import Fill from 'ol/style/Fill'
import Icon from 'ol/style/Icon'
import RegularShape from 'ol/style/RegularShape'
import Stroke from 'ol/style/Stroke'
import Style from 'ol/style/Style'
import Text from 'ol/style/Text'
import { copyDeep, copy } from './utilitiesObject'
import { checkFor } from './utilities'
import { Debug } from './Debug'
import { parseCSSColor } from 'csscolorparser'
import { isFunction, isObject, isArray } from 'lodash/lang'
/**
* @typedef {string|StyleObject|ol.style.Style} StyleLike
*/
/**
* @typedef {Object} StyleObject
*/
/**
* merges two style configs
* @param {StyleObject} configTarget
* @param {StyleObject} configSource
* @returns {StyleObject}
*/
function mergeStyleConfigs (configTarget, configSource) {
const mergedConf = copyDeep(configTarget)
if (configSource) {
for (const k of Object.keys(configSource)) {
const sourceProp = configSource[k]
if (configTarget.hasOwnProperty(k)) {
const targetProp = configTarget[k]
if (typeof targetProp === 'object' && !(targetProp instanceof Array)) {
// if it is another object, merge recursively
const targetProp = configTarget[k]
if (targetProp.hasOwnProperty('type')) {
if (sourceProp.hasOwnProperty('type')) {
if (configTarget[k].type === sourceProp.type) {
mergedConf[k] = mergeStyleConfigs(targetProp, sourceProp)
}
}
} else {
mergedConf[k] = mergeStyleConfigs(targetProp, sourceProp)
}
}
} else {
// copy over if it doesn't exist in the target
if (typeof sourceProp === 'object' && !(sourceProp instanceof Array)) {
mergedConf[k] = copyDeep(sourceProp)
} else {
mergedConf[k] = sourceProp
}
}
}
}
return mergedConf
}
/**
* This class coordinates the styling.
*/
export class Styling {
/**
* @param {Object} [options]
* @param {Object} [options.styleConfigMap]
* @param {number} [options.scaleIcons]
* @param {boolean} [options.manageStyles=true]
*/
constructor (options = {}) {
/**
* @type {Map.<string,StyleObject>}
* @private
*/
this.styleConfigMap_ = new Map()
if (!this.styleConfigMap_.has('#defaultStyle')) {
// FallbackStyle
this.styleConfigMap_.set('#defaultStyle', {
stroke: {
color: 'rgba(0,0,0,0.9)',
width: 2
},
fill: {
color: 'rgba(0,0,0,0.3)'
},
image: {
type: 'circle',
stroke: {
color: 'rgba(0,0,0,0.9)',
width: 2
},
fill: {
color: 'rgba(0,0,0,0.3)'
}
}
})
}
if (options.styleConfigMap) {
for (const k of Object.keys(options.styleConfigMap)) {
this.styleConfigMap_.set(k, options.styleConfigMap[k])
}
}
/**
* @type {Map.<string,ol.style.Style>}
* @private
*/
this.styleMap_ = new Map()
/**
* @type {Set.<ol.style.Style>}
* @private
*/
this.allStyles_ = new Set()
if (options.scaleIcons) {
this.setGlobalIconScale(options.scaleIcons)
}
/**
* @param {ol.Feature} feature
* @param {number} resolution
* @returns {*}
* @private
*/
this.managingFeatureStyle_ = (feature, resolution) => {
return this.processedStyle(feature.get('managedStyle'), feature, resolution)
}
this.nullStyle_ = new Style({
image: null
})
this.manageStyles_ = options.manageStyles !== false
}
manifestStyle (style, feature, resolution) {
while ($.isFunction(style)) {
style = style(feature, resolution)
}
if (!style) {
style = this.getStyle('#defaultStyle')
}
if (isArray(style)) {
return style.map(s => this.manifestStyle(s))
} else {
return style
}
}
/**
* @param {number} scale
*/
setGlobalIconScale (scale) {
/**
* @type {number}
* @private
*/
this.globalIconScale_ = scale
}
/**
* @returns {number}
*/
getGlobalIconScale () {
return this.globalIconScale_ || 1
}
/**
* @param {StyleObject} styleConf
* @returns {ol.style.Style}
*/
getStyleFromConfig (styleConf) {
const filledStyleConf = mergeStyleConfigs(styleConf, this.styleConfigMap_.get('#default'))
function addFillsAndStrokes (subStyleConf) {
subStyleConf = subStyleConf || {}
const preparedOptions = copy(subStyleConf)
if (checkFor(subStyleConf, 'fill')) {
preparedOptions.fill = new Fill(mergeStyleConfigs(subStyleConf.fill, filledStyleConf.fill))
} else {
preparedOptions.fill = new Fill(filledStyleConf.fill)
}
if (checkFor(subStyleConf, 'backgroundFill')) {
preparedOptions.backgroundFill =
new Fill(mergeStyleConfigs(subStyleConf.backgroundFill, filledStyleConf.backgroundFill))
}
if (checkFor(subStyleConf, 'stroke')) {
preparedOptions.stroke = new Stroke(mergeStyleConfigs(subStyleConf.stroke, filledStyleConf.stroke))
} else {
preparedOptions.stroke = new Stroke(filledStyleConf.stroke)
}
if (checkFor(subStyleConf, 'backgroundStroke')) {
preparedOptions.backgroundStroke =
new Stroke(mergeStyleConfigs(subStyleConf.backgroundStroke, filledStyleConf.backgroundStroke))
}
return preparedOptions
}
const styleOptions = addFillsAndStrokes(filledStyleConf)
let getTextProperty
if (filledStyleConf.hasOwnProperty('text')) {
getTextProperty = filledStyleConf.text.textProperty
styleOptions.text = new Text(addFillsAndStrokes(filledStyleConf.text))
}
let scalable = false
if (filledStyleConf.hasOwnProperty('image')) {
if (filledStyleConf.image.type === 'icon' &&
(filledStyleConf.image.hasOwnProperty('src')) && filledStyleConf.image.src) {
styleOptions.image = new Icon(filledStyleConf.image)
scalable = true
} else if (filledStyleConf.image.type === 'circle') {
styleOptions.image = new Circle(addFillsAndStrokes(filledStyleConf.image))
scalable = true
} else if (filledStyleConf.image.type === 'regularShape') {
styleOptions.image = new RegularShape(addFillsAndStrokes(filledStyleConf.image))
scalable = true
}
if (scalable) {
styleOptions.image.setScale((styleOptions.image.getScale() || 1) * this.getGlobalIconScale())
}
}
const style = new Style(styleOptions)
if (getTextProperty) {
return feature => {
const text = feature.get(getTextProperty)
if (text) {
style.getText().setText(text)
} else {
style.getText().setText()
}
return style
}
} else {
return style
}
}
getConfigFromStyle () {
throw new Error('Not implemented yet')
}
/**
* @param {string} id
* @returns {ol.style.Style}
*/
getStyleById (id) {
if (!this.styleMap_.has(id)) {
if (this.styleConfigMap_.has(id)) {
this.styleMap_.set(id, this.getStyleFromConfig(this.getConfigById(id)))
} else {
Debug.warn('No style found for id ' + id + '. Using default style.')
return this.styleMap_.get('#defaultStyle')
}
}
return this.styleMap_.get(id)
}
/**
* @param {string} id
* @returns {StyleObject}
*/
getConfigById (id) {
if (this.styleConfigMap_.has(id)) {
return this.styleConfigMap_.get(id)
} else {
Debug.warn('No style config found for id ' + id + '. Using default style.')
return this.styleConfigMap_.get('#defaultStyle')
}
}
/**
* @param {StyleLike} data
* @returns {ol.style.Style}
*/
getStyle (data) {
if (data === undefined) {
return this.getStyleById('#defaultStyle')
} else if (data instanceof Style || isFunction(data)) {
return data
} else if (isArray(data)) {
return data.map(d => this.getStyle(d))
} else if (isObject(data)) {
if (data.hasOwnProperty('conditional')) {
return this.getConditionalStyleFromConfig(data.conditional)
} else {
return this.getStyleFromConfig(data)
}
} else {
return this.getStyleById(data)
}
}
/**
* This internal method is called to adjust each style to current global and feature settings
* @param feature
* @param style
* @returns {ol.style.Style}
* @private
*/
adjustStyle_ (feature, style) {
if (!feature.get('hidden')) {
if (this.getGlobalIconScale() !== 1 || feature.get('opacity') !== undefined) {
const clone = style.clone()
this.scaleStyle_(clone)
if (feature.get('opacity') !== undefined) {
this.changeColorOpacity_(clone, feature.get('opacity'))
}
return clone
} else {
return style
}
} else {
return this.nullStyle_
}
}
/**
* This method adjusts the scale of a style
* @param style
* @private
*/
scaleStyle_ (style) {
const image = style.getImage()
if (image) {
const origScale = style.getImage().getScale() || 1
image.setScale(origScale * this.getGlobalIconScale())
}
}
/**
* adjust the styles opacity by a given value
* @param {ol.style.Style} style
* @param {number} opacity between 0 and 1
* @returns {ol.style.Style}
*/
changeColorOpacity_ (style, opacity) {
const adjustColor = (style, opacity) => {
let color = style.getColor()
if (color !== null) {
if (!(color instanceof Array)) {
if (typeof color === 'string') {
color = parseCSSColor(color)
} else {
throw new Error('Type not supported')
}
}
color[3] = color[3] * opacity
}
style.setColor(color)
}
if (style.getImage()) {
style.getImage().setOpacity(opacity)
}
if (style.getFill()) {
adjustColor(style.getFill(), opacity)
}
if (style.getStroke()) {
adjustColor(style.getStroke(), opacity)
}
if (style.getText()) {
if (style.getText().getFill()) {
adjustColor(style.getText().getFill(), opacity)
}
if (style.getText().getStroke()) {
adjustColor(style.getText().getStroke(), opacity)
}
}
}
manageFeature (feature) {
if (this.manageStyles_) {
const style = feature.getStyle()
if (style && !feature.get('managedStyle')) {
feature.set('managedStyle', style)
feature.setStyle(this.managingFeatureStyle_)
}
}
}
manageFeatureCollection (collection) {
if (this.manageStyles_) {
collection.forEach(feature => {
this.manageFeature(feature)
})
collection.on('add', e => {
this.manageFeature(e.element)
})
}
}
processedStyle (style, feature, resolution) {
const mStyle = this.manifestStyle(style, feature, resolution)
if (isArray(mStyle)) {
return mStyle.map(s => this.adjustStyle_(feature, s))
} else {
return this.adjustStyle_(feature, mStyle)
}
}
manageLayer (layer) {
if (this.manageStyles_) {
const style = layer.getStyle()
if (style && !layer.get('managedStyle')) {
layer.set('managedStyle', style)
layer.setStyle((feature, resolution) => {
return this.processedStyle(style, feature, resolution)
})
}
layer.getSource().getFeatures().forEach(feature => {
this.manageFeature(feature)
})
layer.getSource().on('addfeature', e => {
this.manageFeature(e.feature)
})
}
}
getConditionalStyleFromConfig (configArr) {
const styles = configArr.map(o => this.getStyle(o.style))
return feature => {
for (let i = 0; i < configArr.length; i++) {
if (!configArr[i].condition) {
return styles[i]
}
const cond = configArr[i].condition
switch (cond[1]) {
case '=':
if (feature.get(cond[0]) === cond[2]) {
return styles[i]
}
break
case '!=':
if (feature.get(cond[0]) !== cond[2]) {
return styles[i]
}
break
case '<':
if (feature.get(cond[0]) < cond[2]) {
return styles[i]
}
break
case '>':
if (feature.get(cond[0]) > cond[2]) {
return styles[i]
}
break
case '<=':
if (feature.get(cond[0]) <= cond[2]) {
return styles[i]
}
break
case '>=':
if (feature.get(cond[0]) >= cond[2]) {
return styles[i]
}
break
}
}
}
}
}