/**
* @module state/MapLayer.js
* @name MapLayerState
* @copyright 2023 3Liz
* @author DHONT René-Luc
* @license MPL-2.0
*/
import EventDispatcher from './../../utils/EventDispatcher.js';
import { createEnum } from './../utils/Enums.js';
import { LayerConfig } from './../config/Layer.js';
import { AttributionConfig } from './../config/Attribution.js'
import { LayerStyleConfig, LayerGeographicBoundingBoxConfig, LayerBoundingBoxConfig } from './../config/LayerTree.js';
import { LayerItemState, LayerGroupState, LayerLayerState, LayerVectorState, LayerRasterState } from './Layer.js';
import { LayerSymbolsSymbology, LayerIconSymbology, LayerGroupSymbology } from './Symbology.js';
import { ExternalMapGroupState } from './ExternalMapLayer.js'
/**
* Enum for map layer load status
* @readonly
* @enum {string}
* @property {string} Undefined - The map layer's load status is undefined (not yet loading, ready or error)
* @property {string} Loading - The map layer is loading
* @property {string} Ready - The map layer is ready
* @property {string} Error - The map layer's load status is error (an error has been raised during loading)
* @see {@link https://openlayers.org/en/latest/apidoc/module-ol_source_Source.html#~State|OpenLayer Source State}
*/
export const MapLayerLoadStatus = createEnum({
'Undefined': 'undefined',
'Loading': 'loading',
'Ready': 'ready',
'Error': 'error',
});
/**
* Class representing a map item: could be a group or a layer
* @class
* @augments EventDispatcher
*/
export class MapItemState extends EventDispatcher {
/**
* Create a map item
* @param {string} type - the map layer item type
* @param {LayerItemState} layerItemState - the layer item state
* @param {MapItemState} [parentMapGroup] - the parent layer map group
*/
constructor(type, layerItemState, parentMapGroup) {
super();
this._type = type
this._layerItemState = layerItemState;
this._parentMapGroup = null;
if (parentMapGroup instanceof MapItemState
&& parentMapGroup.type == 'group') {
this._parentMapGroup = parentMapGroup;
}
if (layerItemState instanceof LayerGroupState) {
layerItemState.addListener(
this.dispatch.bind(this),
layerItemState.mapType + '.visibility.changed'
);
layerItemState.addListener(
this.dispatch.bind(this),
layerItemState.mapType + '.symbology.changed'
);
layerItemState.addListener(
this.dispatch.bind(this),
layerItemState.mapType + '.opacity.changed'
);
layerItemState.addListener(this.dispatch.bind(this), 'layer.symbol.expanded.changed');
} else {
layerItemState.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.symbology.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.opacity.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.style.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.symbol.checked.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.symbol.expanded.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.selection.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.selection.token.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.filter.changed');
layerItemState.addListener(this.dispatch.bind(this), 'layer.filter.token.changed');
}
}
/**
* Map item name
* @type {string}
*/
get name() {
return this._layerItemState.name;
}
/**
* Map item type
* @type {string}
*/
get type() {
return this._type;
}
/**
* the layer item level
* @type {number}
*/
get level() {
return this._layerItemState.level;
}
/**
* WMS item name
* @type {?string}
*/
get wmsName() {
return this._layerItemState.wmsName;
}
/**
* WMS item title
* @type {string}
*/
get wmsTitle() {
return this._layerItemState.wmsTitle;
}
/**
* WMS item Geographic Bounding Box
* @type {?LayerGeographicBoundingBoxConfig}
*/
get wmsGeographicBoundingBox() {
return this._layerItemState.wmsGeographicBoundingBox;
}
/**
* WMS item Bounding Boxes
* @type {LayerBoundingBoxConfig[]}
*/
get wmsBoundingBoxes() {
return this._layerItemState.wmsBoundingBoxes;
}
/**
* WMS Minimum scale denominator
* If the minimum scale denominator is not defined: -1 is returned
* If the WMS layer is a group, the minimum scale denominator is -1 if only one layer
* minimum scale denominator is not defined else the smallest layer minimum scale denominator
* in the group
* @type {number}
*/
get wmsMinScaleDenominator() {
return this._layerItemState.wmsMinScaleDenominator;
}
/**
* WMS Maximum scale denominator
* If the maximum scale denominator is not defined: -1 is returned
* If the WMS layer is a group, the maximum scale denominator is the largest of the layers in the group
* @type {number}
*/
get wmsMaxScaleDenominator() {
return this._layerItemState.wmsMaxScaleDenominator;
}
/**
* Map item is checked
* @type {boolean}
*/
get checked() {
return this._layerItemState.checked;
}
/**
* Set map item is checked
* @type {boolean}
*/
set checked(val) {
this._layerItemState.checked = val;
}
/**
* Map item is visible
* It depends on the parent visibility
* @type {boolean}
*/
get visibility() {
return this._layerItemState.visibility;
}
/**
* Map item opacity
* @type {number}
*/
get opacity() {
return this._layerItemState.opacity;
}
/**
* Set map item opacity
* @type {number}
*/
set opacity(val) {
this._layerItemState.opacity = val;
}
/**
* Lizmap layer config
* @type {?LayerConfig}
*/
get layerConfig() {
return this._layerItemState.layerConfig;
}
/**
* Lizmap layer item state
* @type {?LayerItemState}
*/
get itemState() {
return this._layerItemState;
}
/**
* Calculate and save visibility
* @returns {boolean} the calculated visibility
*/
calculateVisibility() {
return this._layerItemState.calculateVisibility();
}
}
/**
* Class representing a map group state
* @class
* @augments MapItemState
*/
export class MapGroupState extends MapItemState {
/**
* Creating a map group state instance
* @param {LayerGroupState} layerGroupState - the layer tree group config
* @param {MapGroupState} [parentMapGroup] - the parent layer map group
*/
constructor(layerGroupState, parentMapGroup) {
super('group', layerGroupState, parentMapGroup);
this._items = [];
this._notInLayerTree = [];
for (const layerItem of layerGroupState.getChildren()) {
// Group
if (layerItem instanceof LayerGroupState) {
// Hidden
if (layerItem.name.toLowerCase() == 'hidden') {
continue;
}
// Overview
if (layerItem.name.toLowerCase() == 'overview' && layerItem.level == 1) {
continue;
}
// Baselayers
if (layerItem.name.toLowerCase() == 'baselayers' && layerItem.level == 1) {
continue;
}
// Empty Group
if (layerItem.childrenCount == 0) {
continue;
}
if (!layerItem.groupAsLayer) {
// Build group
const group = new MapGroupState(layerItem, this);
// Manage layer not display in layer tree
if ( this._parentMapGroup != null) {
// Merge them if we are not at the root level
this._notInLayerTree = this._notInLayerTree.concat(...group._notInLayerTree);
} else {
// Insert them in items list at the root level
this._items = this._items.concat(...group._notInLayerTree);
}
// If the group is empty do not keep it
if (group.childrenCount == 0) {
continue;
}
group.addListener(this.dispatch.bind(this), 'group.visibility.changed');
group.addListener(this.dispatch.bind(this), 'group.symbology.changed');
group.addListener(this.dispatch.bind(this), 'group.opacity.changed');
group.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
group.addListener(this.dispatch.bind(this), 'layer.symbology.changed');
group.addListener(this.dispatch.bind(this), 'layer.opacity.changed');
group.addListener(this.dispatch.bind(this), 'layer.loading.changed');
group.addListener(this.dispatch.bind(this), 'layer.load.status.changed');
group.addListener(this.dispatch.bind(this), 'layer.style.changed');
group.addListener(this.dispatch.bind(this), 'layer.symbol.checked.changed');
group.addListener(this.dispatch.bind(this), 'layer.symbol.expanded.changed');
group.addListener(this.dispatch.bind(this), 'layer.selection.changed');
group.addListener(this.dispatch.bind(this), 'layer.selection.token.changed');
group.addListener(this.dispatch.bind(this), 'layer.filter.changed');
group.addListener(this.dispatch.bind(this), 'layer.filter.token.changed');
this._items.push(group);
} else {
// Build group as layer
const layer = new MapLayerState(layerItem, this)
layer.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
layer.addListener(this.dispatch.bind(this), 'layer.symbology.changed');
layer.addListener(this.dispatch.bind(this), 'layer.opacity.changed');
layer.addListener(this.dispatch.bind(this), 'layer.loading.changed');
layer.addListener(this.dispatch.bind(this), 'layer.load.status.changed');
layer.addListener(this.dispatch.bind(this), 'layer.style.changed');
layer.addListener(this.dispatch.bind(this), 'layer.symbol.checked.changed');
layer.addListener(this.dispatch.bind(this), 'layer.symbol.expanded.changed');
layer.addListener(this.dispatch.bind(this), 'layer.selection.changed');
layer.addListener(this.dispatch.bind(this), 'layer.selection.token.changed');
layer.addListener(this.dispatch.bind(this), 'layer.filter.changed');
layer.addListener(this.dispatch.bind(this), 'layer.filter.token.changed');
this._items.push(layer);
}
} else if (layerItem instanceof LayerLayerState && !layerItem.layerConfig.baseLayer) {
// layer with geometry type equal to 'none' or 'unknown' cannot be displayed
if (layerItem instanceof LayerVectorState
&& !layerItem.isSpatial) {
continue;
}
// layer not display in legend and not toggled has not to be displayed
if (!layerItem.displayInLegend && !layerItem.layerConfig.toggled) {
continue;
}
// Build layer
const layer = new MapLayerState(layerItem, this)
// Store layer not display in layer tree if we are not at the root level
if (!layerItem.displayInLegend && this._parentMapGroup != null) {
this._notInLayerTree.push(layer);
} else {
this._items.push(layer);
layer.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
layer.addListener(this.dispatch.bind(this), 'layer.symbology.changed');
layer.addListener(this.dispatch.bind(this), 'layer.opacity.changed');
layer.addListener(this.dispatch.bind(this), 'layer.loading.changed');
layer.addListener(this.dispatch.bind(this), 'layer.load.status.changed');
layer.addListener(this.dispatch.bind(this), 'layer.style.changed');
layer.addListener(this.dispatch.bind(this), 'layer.symbol.checked.changed');
layer.addListener(this.dispatch.bind(this), 'layer.symbol.expanded.changed');
layer.addListener(this.dispatch.bind(this), 'layer.selection.changed');
layer.addListener(this.dispatch.bind(this), 'layer.selection.token.changed');
layer.addListener(this.dispatch.bind(this), 'layer.filter.changed');
layer.addListener(this.dispatch.bind(this), 'layer.filter.token.changed');
}
}
}
}
/**
* The layer mutually exclusive activation (group only)
* @type {boolean}
*/
get mutuallyExclusive() {
if (this.layerConfig == null) {
return false;
}
return this.layerConfig.mutuallyExclusive;
}
/**
* Children items count
* @type {number}
*/
get childrenCount() {
return this._items.length;
}
/**
* Children items
* @type {MapItemState[]}
*/
get children() {
return [...this._items];
}
/**
* Iterate through children items
* @generator
* @yields {MapItemState} The next child item
*/
*getChildren() {
for (const item of this._items) {
yield item;
}
}
/**
* Find layer names
* @returns {string[]} The layer names of all map layers
*/
findMapLayerNames() {
let names = []
for(const item of this.getChildren()) {
if (item instanceof MapLayerState) {
names.push(item.name);
} else if (item instanceof MapGroupState) {
names = names.concat(item.findMapLayerNames());
}
}
return names;
}
/**
* Count map layers by exploding the layers within every "group as layer" groups.
* Used to get the actual total layers number for ordering purpose (i.e. assign correct zIndex value to each map layer)
* @returns {number} The exploded layers count
*/
countExplodedMapLayers() {
let layersCount = 0;
for(const item of this.getChildren()) {
if (item instanceof MapLayerState) {
const itemState = item.itemState;
if(itemState instanceof LayerGroupState && itemState.groupAsLayer){
//count the layers inside the group
layersCount += itemState.findLayers().length;
}
else if(itemState instanceof LayerLayerState){
layersCount++;
}
} else if (item instanceof MapGroupState) {
layersCount += item.countExplodedMapLayers();
}
}
return layersCount;
}
/**
* Find layer items
* @returns {MapLayerState[]} The layer names of all map layers
*/
findMapLayers() {
let items = []
for(const item of this.getChildren()) {
if (item instanceof MapLayerState) {
items.push(item);
} else if (item instanceof MapGroupState) {
items = items.concat(item.findMapLayers());
}
}
return items;
}
/**
* Find layer and group items
* @returns {Array<MapLayerState|MapGroupState>} All map layers and map group states
*/
findMapLayersAndGroups() {
let items = []
for(const item of this.getChildren()) {
if (item instanceof MapLayerState) {
items.push(item);
} else if (item instanceof MapGroupState) {
items.push(item);
items = items.concat(item.findMapLayersAndGroups());
}
}
return items;
}
/**
* Get layer item by its name
* @param {string} name - the layer name
* @returns {MapLayerState} The MapLayerState associated to the name
*/
getMapLayerByName(name) {
for (const layer of this.findMapLayers()) {
if(layer.name === name) {
return layer;
}
}
throw RangeError('The layer name `'+ name +'` is unknown!');
}
/**
* Get layer or group item by its name
* @param {string} name - the layer or group name
* @returns {MapLayerState|MapGroupState} The MapLayerState or MapGroupState associated to the name
*/
getMapLayerOrGroupByName(name) {
for (const item of this.findMapLayersAndGroups()) {
if(item.name === name) {
return item;
}
}
throw RangeError('The layer or group name `'+ name +'` is unknown!');
}
}
/**
* Class representing a map layer state
* @class
* @augments MapItemState
*/
export class MapLayerState extends MapItemState {
/**
* Creating a map layer state instance
* @param {LayerVectorState|LayerRasterState|LayerGroupState} layerItemState - the layer item state
* @param {MapGroupState} [parentMapGroup] - the parent layer map group
*/
constructor(layerItemState, parentMapGroup) {
super('layer', layerItemState, parentMapGroup);
// The layer is group
if (this.itemState instanceof LayerGroupState) {
this.itemState.addListener(this.dispatch.bind(this), 'layer.selection.changed');
this.itemState.addListener(this.dispatch.bind(this), 'layer.selection.token.changed');
this.itemState.addListener(this.dispatch.bind(this), 'layer.filter.changed');
this.itemState.addListener(this.dispatch.bind(this), 'layer.filter.token.changed');
}
// load the layer in a single ImageWMS layer
this._singleWMSLayer = false;
// set isLoading to false
this._loading = false;
this._loadStatus = MapLayerLoadStatus.Undefined;
}
/**
* vector layer is loaded in a single layer ImageLayer or not
* @type {boolean}
*/
get singleWMSLayer(){
return this._singleWMSLayer;
}
/**
* set if the map layer is loaded in a single ImageWMS layer or not
* @type {boolean}
*/
set singleWMSLayer(val){
this._singleWMSLayer = val;
}
/**
* Layer type
* @type {string}
*/
get layerType() {
if (this.itemState instanceof LayerGroupState) {
return 'group';
}
return this._layerItemState.layerType;
}
/**
* Layer order from top to bottom
* @type {number}
*/
get layerOrder() {
return this._layerItemState.layerOrder;
}
/**
* Is the map layer displayed in layer tree
* @type {boolean}
*/
get displayInLayerTree() {
return this._layerItemState.displayInLegend;
}
/**
* Vector layer has selected features
* The selected features is not empty
* @type {boolean}
*/
get hasSelectedFeatures() {
if (this._layerItemState instanceof LayerVectorState) {
return this._layerItemState.hasSelectedFeatures;
}
return false;
}
/**
* Vector layer is filtered
* The expression filter is not null
* @type {boolean}
*/
get isFiltered() {
if (this._layerItemState instanceof LayerVectorState) {
return this._layerItemState.isFiltered;
}
return false;
}
/**
* WMS selected layer style name
* @type {string}
*/
get wmsSelectedStyleName() {
// The map layer is not a group
if (!this._layerItemState.groupAsLayer) {
return this._layerItemState.wmsSelectedStyleName;
}
// WMS Style for group as layer
return '';
}
/**
* Update WMS selected layer style name
* based on wmsStyles list
* @param {string} styleName - The WMS layer style name to select
*/
set wmsSelectedStyleName(styleName) {
// The map layer is not a group
if (!this._layerItemState.groupAsLayer) {
this._layerItemState.wmsSelectedStyleName = styleName;
return;
}
if (styleName !== '') {
throw TypeError('Cannot assign an unknown WMS style name! `'+styleName+'` is not in the layer `'+this.name+'` WMS styles!');
}
}
/**
* WMS layer styles
* @type {LayerStyleConfig[]}
*/
get wmsStyles() {
if ( this._layerItemState.type == 'layer' ) {
return this._layerItemState.wmsStyles;
}
// WMS Style for group as layer
return [new LayerStyleConfig('', '')];
}
/**
* WMS layer attribution
* @type {?AttributionConfig}
*/
get wmsAttribution() {
if ( this._layerItemState.type == 'layer' ) {
return this._layerItemState.wmsAttribution;
}
return null;
}
/**
* Parameters for OGC WMS Request
* @type {object}
*/
get wmsParameters() {
// The map layer is not a group
if (!this._layerItemState.groupAsLayer) {
return this._layerItemState.wmsParameters;
}
return {
'LAYERS': this.wmsName,
'STYLES': this.wmsSelectedStyleName,
'FORMAT': this.layerConfig.imageFormat,
'DPI': 96
};
}
/**
* Layer symbology
* @type {?(LayerIconSymbology|LayerSymbolsSymbology|LayerGroupSymbology)}
*/
get symbology() {
return this._layerItemState.symbology;
}
/**
* Update layer symbology
* @param {(object | LayerIconSymbology | LayerSymbolsSymbology | LayerGroupSymbology)} node - The symbology node
*/
set symbology(node) {
this._layerItemState.symbology = node;
}
/**
* The layer load status
* @see MapLayerLoadStatus
* @type {string}
*/
get loadStatus() {
return this._loadStatus;
}
/**
* Set layer load status
* @see MapLayerLoadStatus
* @param {string} status - Expected values provided by the map layer load status enum
*/
set loadStatus(status) {
const statusKeys = Object.keys(MapLayerLoadStatus).filter(key => MapLayerLoadStatus[key] === status);
if (statusKeys.length != 1) {
throw new TypeError('Unkonw status: `'+status+'`!');
}
// No changes
if (this._loadStatus == status) {
return;
}
// Set new value
this._loadStatus = status;
this.dispatch({
type: 'layer.load.status.changed',
name: this.name,
loadStatus: this.loadStatus,
})
}
}
/**
* Class representing a map group state as map root
* @class
* @augments MapGroupState
*/
export class MapRootState extends MapGroupState {
/**
* Creating a map root state instance
* @param {LayerGroupState} layerGroupState - the layer tree group config
*/
constructor(layerGroupState) {
super(layerGroupState);
}
/**
* Create an external map group state
* @param {string} name - the external map group name
* @returns {ExternalMapGroupState} The external map group state
*/
createExternalGroup(name) {
// Checks that name is unknown
const groups = this._items
.map((item, index) => {return {'name': item.name, 'type': item.type,'index':index}})
.filter((item) => item.type == 'ext-group' && item.name == name);
if (groups.length != 0) {
throw RangeError('The group name `'+ name +'` is already used by an external group child!');
}
const extGroup = new ExternalMapGroupState(name);
extGroup.addListener(this.dispatch.bind(this), 'ext-group.wmsTitle.changed');
extGroup.addListener(this.dispatch.bind(this), 'ext-group.visibility.changed');
extGroup.addListener(this.dispatch.bind(this), 'ol-layer.wmsTitle.changed');
extGroup.addListener(this.dispatch.bind(this), 'ol-layer.icon.changed');
extGroup.addListener(this.dispatch.bind(this), 'ol-layer.opacity.changed');
extGroup.addListener(this.dispatch.bind(this), 'ol-layer.visibility.changed');
this._items.unshift(extGroup);
this.dispatch({
type: 'ext-group.added',
name: name,
});
return extGroup;
}
/**
* Create an external map group state
* @param {string} name - the external map group name to remove
* @returns {ExternalMapGroupState|undefined} The removed external map group or undefined if the name is unknown
*/
removeExternalGroup(name) {
const groups = this._items
.map((item, index) => {return {'name': item.name, 'type': item.type,'index':index}})
.filter((item) => item.type == 'ext-group' && item.name == name);
if (groups.length == 0) {
return undefined;
}
const extGroup = this._items.at(groups[0].index);
extGroup.removeListener(this.dispatch.bind(this), 'ext-group.wmsTitle.changed');
extGroup.removeListener(this.dispatch.bind(this), 'ext-group.visibility.changed');
extGroup.removeListener(this.dispatch.bind(this), 'ol-layer.wmsTitle.changed');
extGroup.removeListener(this.dispatch.bind(this), 'ol-layer.icon.changed');
extGroup.removeListener(this.dispatch.bind(this), 'ol-layer.opacity.changed');
extGroup.removeListener(this.dispatch.bind(this), 'ol-layer.visibility.changed');
this._items.splice(groups[0].index, 1);
this.dispatch({
type: 'ext-group.removed',
name: name,
});
extGroup.clean();
return extGroup;
}
}