import React from 'react'
import PropTypes from 'prop-types'
import StyleManager from 'LayerStyler/StyleManager'
import HeatmapLayer from 'ol/layer/Heatmap'
import olFormatFilterAnd from 'ol/format/filter/And'
import olFormatFilterOr from 'ol/format/filter/Or'
import olFormatFilterEqualTo from 'ol/format/filter/EqualTo'
import olFormatFilterIsLike from 'ol/format/filter/IsLike'
import olFilterFunction from '../classes/FilterFunction'
import ugh from 'ugh'
import escapeRegExp from 'lodash.escaperegexp'
import { addMovementListener, removeMovementListener } from 'Popup'
import { connectToContext } from 'Provider'
/**
* UI to choose color, stroke, fill etc. for styles and labels on layers
* @component
* @category LayerStyler
*/
class LayerStyler extends React.Component {
constructor (props) {
super(props)
this.state = {
attributeValues: [],
listeners: [],
whitelistedLayers: props.whitelistedLayers
}
}
componentDidMount () {
const { map } = this.props
const layers = map.getLayers()
// if a layer is added or removed, the list of layers should be updated
const handleLayersChange = (e) => this.forceUpdate()
// bind the event listeners
const onAddKey = layers.on('add', handleLayersChange)
const onRemoveKey = layers.on('remove', handleLayersChange)
// make sure the attributes get updated each time the view extent changes
const listeners = addMovementListener(map, () => this.forceUpdate())
this.setState({ listeners: [...listeners, onAddKey, onRemoveKey] })
}
componentWillUnmount () {
const { listeners } = this.state
removeMovementListener(listeners)
}
getTitleForLayer = (layer) => {
return layer?.get?.('title') || layer?.getTypeName?.() || 'untitled layer'
}
getAttributesForLayer = layer => {
if (layer) {
if (layer.isGeoserverLayer) {
return layer.getAttributes().sort((a, b) => a.localeCompare(b))
} else if (layer.isVectorLayer || layer.isVectorTileLayer) {
return layer.getAttributes().sort((a, b) => a.localeCompare(b))
}
}
}
getValuesForAttribute = async (layer, attribute) => {
if (layer) {
if (layer.isGeoserverLayer) {
const { typeName } = layer
const whitelistedLayer = this.state.whitelistedLayers.find(l => l.typename === typeName)
const opts = whitelistedLayer ? { commaDelimitedAttributes: whitelistedLayer.commaDelimitedAttributes } : {}
const attributeValues = await layer.fetchValuesForAttribute(this.props.map, attribute, opts)
this.setState({ attributeValues })
} else if (layer.isVectorLayer || layer.isVectorTileLayer) {
const attributeValues = layer.fetchValuesForAttribute(attribute)
this.setState({ attributeValues })
}
}
}
getCommaDelimitedAttributesForLayer = (layer) => {
if (layer && layer.isGeoserverLayer) {
const { typeName } = layer
const whitelistedLayer = this.state.whitelistedLayers.find(l => l.typename === typeName)
return whitelistedLayer ? whitelistedLayer.commaDelimitedAttributes : []
}
}
onFilterChange = (layer, filters) => {
if (layer && layer.isGeoserverLayer) {
const { typeName } = layer
const whitelistedLayer = this.state.whitelistedLayers.find(l => l.typename === typeName)
const opts = whitelistedLayer ? { commaDelimitedAttributes: whitelistedLayer.commaDelimitedAttributes } : {}
layer.setWMSFilters(filters, opts)
layer.setOlFilters(this.setOlFilters(filters, opts))
// cause a re-render which will re-hydrate the latest styles
this.forceUpdate()
}
}
setOlFilters = (filters = [], opts) => {
const olFilters = filters.map(({ attribute, value }) => {
if (opts.commaDelimitedAttributes && opts.commaDelimitedAttributes.includes(attribute)) {
// this filter is due to commaDelimitedAttributes, we have to make an OR filter with 3 regex filters
// the olFilterFunction is our custom filter function due to openlayers not supporting functions as filters
return new olFormatFilterOr(
new olFormatFilterIsLike(new olFilterFunction('strMatches', attribute, `^${escapeRegExp(value)}( )??,.*`), true, '*', '.', '!'),
new olFormatFilterIsLike(new olFilterFunction('strMatches', attribute, `.*,( )??${escapeRegExp(value)}( )??,.*`), true, '*', '.', '!'),
new olFormatFilterIsLike(new olFilterFunction('strMatches', attribute, `.*,( )??${escapeRegExp(value)}( )??,.*`), true, '*', '.', '!'),
new olFormatFilterEqualTo(attribute, escapeRegExp(value))
)
} else {
return new olFormatFilterEqualTo(attribute, value)
}
})
if (filters[0]?.logical === 'AND' && filters.length > 1) {
return new olFormatFilterAnd(...olFilters)
} else if (filters[0]?.logical === 'OR' && filters.length > 1) {
return new olFormatFilterOr(...olFilters)
} else {
return olFilters[0]
}
}
onDefaultStyleChange = (layer, styles) => {
if (layer && layer.isGeoserverLayer) {
layer.setDefaultWMSStyles(styles)
} else {
layer.updateDefaultVectorStyles(styles)
}
// cause a re-render which will re-hydrate the latest styles
this.forceUpdate()
}
onUserStyleChange = (layer, styles) => {
if (layer && layer.isGeoserverLayer) {
layer.setUserWMSStyles(styles)
} else if (layer && (layer.isVectorLayer || layer.isVectorTileLayer)) {
layer.setUserVectorStyles(styles)
}
// cause a re-render which will re-hydrate the latest styles
this.forceUpdate()
}
onDefaultStyleReset = (layer) => {
if (layer && layer.isGeoserverLayer) {
layer.resetDefaultWMSStyles()
} else if (layer.isVectorLayer || layer.isVectorTileLayer) {
layer.resetDefaultVectorStyles()
}
// cause a re-render which will re-hydrate the latest styles
this.forceUpdate()
}
getValidLayers = () => {
const { map } = this.props
const layers = map.getLayers().getArray()
const validLayers = layers.filter(layer => {
return !layer.get('_ol_kit_basemap') && (layer.isGeoserverLayer || layer.isVectorLayer || layer.isVectorTileLayer || layer instanceof HeatmapLayer)
})
if (layers.length - validLayers.length > 1) {
ugh.warn('In order to use ManageLayers, the layer must be either an VectorLayer or GeoserverLayer')
}
return validLayers
}
render () {
const layers = this.getValidLayers()
const { attributeValues } = this.state
const { translations } = this.props
return (
<StyleManager
layers={layers}
translations={translations}
filters={layers.map(l => l.isGeoserverLayer && l.getWMSFilters())}
userStyles={layers.map(l => l.isGeoserverLayer ? l.getUserWMSStyles() : l.getUserVectorStyles?.())}
defaultStyles={layers.map(l => l.isGeoserverLayer ? l.getDefaultWMSStyles() : l.getDefaultVectorStyles?.())}
getCommaDelimitedAttributesForLayer={this.getCommaDelimitedAttributesForLayer}
getTitleForLayer={this.getTitleForLayer}
getValuesForAttribute={this.getValuesForAttribute}
getAttributesForLayer={this.getAttributesForLayer}
attributeValues={attributeValues}
onFilterChange={this.onFilterChange}
onUserStyleChange={this.onUserStyleChange}
onDefaultStyleChange={this.onDefaultStyleChange}
onDefaultStyleReset={this.onDefaultStyleReset}
{...this.props} />
)
}
}
LayerStyler.defaultProps = {
whitelistedLayers: []
}
LayerStyler.propTypes = {
/** Openlayers map object */
map: PropTypes.object.isRequired,
/** Object with key/value pairs for translated strings */
translations: PropTypes.object.isRequired,
/** An array of layer typenames that will be used for a whitelist */
whitelistedLayers: PropTypes.array
}
export default connectToContext(LayerStyler)