/**
* @module state/LayerTree.js
* @name LayerTreeState
* @copyright 2023 3Liz
* @author DHONT René-Luc
* @license MPL-2.0
*/
import EventDispatcher from './../../utils/EventDispatcher.js';
import { LayerConfig } from './../config/Layer.js';
import { AttributionConfig } from './../config/Attribution.js'
import { LayerStyleConfig, LayerGeographicBoundingBoxConfig, LayerBoundingBoxConfig } from './../config/LayerTree.js';
import { MapItemState, MapGroupState, MapLayerState } from './MapLayer.js';
import { ExternalLayerTreeGroupState } from './ExternalLayerTree.js';
import { getDefaultLayerIcon, LayerIconSymbology, LayerSymbolsSymbology, LayerGroupSymbology, SymbolIconSymbology, BaseIconSymbology, BaseSymbolsSymbology } from './Symbology.js';
import { convertBoolean } from './../utils/Converters.js';
/**
* Class representing a layer tree item
* @class
* @augments EventDispatcher
*/
export class LayerTreeItemState extends EventDispatcher {
/**
* Instantiate a layer tree item
* @param {MapItemState} mapItemState - the map item state
* @param {LayerTreeItemState} [parentGroupState] - the parent layer tree group
*/
constructor(mapItemState, parentGroupState) {
super();
this._mapItemState = mapItemState;
this._parentGroupState = null;
this._expanded = false;
if (this.type === "group") {
this._expanded = true;
} else {
this._expanded = this.layerConfig.legendImageOption === "expand_at_startup";
}
if (parentGroupState instanceof LayerTreeItemState
&& parentGroupState.type == 'group') {
this._parentGroupState = parentGroupState;
}
if (mapItemState instanceof MapLayerState) {
mapItemState.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.symbology.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.opacity.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.loading.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.load.status.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.style.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.symbol.checked.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.symbol.expanded.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.selection.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.selection.token.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.filter.changed');
mapItemState.addListener(this.dispatch.bind(this), 'layer.filter.token.changed');
} else if (mapItemState instanceof MapGroupState) {
mapItemState.addListener(this.dispatch.bind(this), 'group.visibility.changed');
mapItemState.addListener(this.dispatch.bind(this), 'group.symbology.changed');
mapItemState.addListener(this.dispatch.bind(this), 'group.opacity.changed');
}
}
/**
* Config layers
* @type {string}
*/
get name() {
return this._mapItemState.name;
}
/**
* Config layers
* @type {string}
*/
get type() {
return this._mapItemState.type;
}
/**
* Layer tree item level
* @type {number}
*/
get level() {
return this._mapItemState.level;
}
/**
* WMS layer name
* @type {?string}
*/
get wmsName() {
return this._mapItemState.wmsName;
}
/**
* WMS layer title
* @type {string}
*/
get wmsTitle() {
return this._mapItemState.wmsTitle;
}
/**
* WMS layer Geographic Bounding Box
* @type {?LayerGeographicBoundingBoxConfig}
*/
get wmsGeographicBoundingBox() {
return this._mapItemState.wmsGeographicBoundingBox;
}
/**
* WMS layer Bounding Boxes
* @type {LayerBoundingBoxConfig[]}
*/
get wmsBoundingBoxes() {
return this._mapItemState.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._mapItemState.wmsMinScaleDenominator;
}
/**
* WMS layer 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._mapItemState.wmsMaxScaleDenominator;
}
/**
* Layer tree item is checked
* @type {boolean}
*/
get checked() {
return this._mapItemState.checked;
}
/**
* Set layer tree item is checked
* @type {boolean}
*/
set checked(val) {
this._mapItemState.checked = val;
}
/**
* Layer tree item is visible
* It depends on the parent visibility
* @type {boolean}
*/
get visibility() {
return this._mapItemState.visibility;
}
/**
* Layer tree item opacity
* @type {number}
*/
get opacity() {
return this._mapItemState.opacity;
}
/**
* Set layer tree item opacity
* @type {number}
*/
set opacity(val) {
this._mapItemState.opacity = val;
}
/**
* Lizmap layer config
* @type {?LayerConfig}
*/
get layerConfig() {
return this._mapItemState.layerConfig;
}
/**
* Map item state
* @type {?MapItemState}
*/
get mapItemState() {
return this._mapItemState;
}
/**
* Layer tree item is expanded
* @type {boolean}
*/
get expanded() {
return this._expanded;
}
/**
* Set layer tree item is expanded
* @type {boolean}
*/
set expanded(val) {
const newVal = convertBoolean(val);
if(this._expanded === newVal){
return;
}
this._expanded = newVal;
this.dispatch({
type: this.type + '.expanded.changed',
name: this.name
});
}
/**
* Calculate and save visibility
* @returns {boolean} the calculated visibility
*/
calculateVisibility() {
return this._mapItemState.calculateVisibility();
}
/**
* Get item visibility taking care of this.visibility and scale
* @param {number} scaleDenominator the scale denominator for which the visibility has to be evaluated
* @returns {boolean} the item visibility
*/
isVisible(scaleDenominator) {
if (this.type === 'group') {
return this.visibility;
}
if(this._mapItemState.wmsMinScaleDenominator !== undefined && this._mapItemState.wmsMaxScaleDenominator !== undefined){
return this.visibility && this._mapItemState.wmsMinScaleDenominator < scaleDenominator
&& scaleDenominator < this._mapItemState.wmsMaxScaleDenominator;
}
return this.visibility;
}
}
/**
* Class representing a layer tree group
* @class
* @augments LayerTreeItemState
*/
export class LayerTreeGroupState extends LayerTreeItemState {
/**
* Instantiate a layer tree group
* @param {MapGroupState} mapGroupState - the map layer group state
* @param {LayerTreeGroupState} [parentGroupState] - the parent layer tree group
*/
constructor(mapGroupState, parentGroupState) {
super(mapGroupState, parentGroupState);
this._items = [];
for (const mapItemState of mapGroupState.getChildren()) {
if (mapItemState instanceof MapGroupState) {
// Build group
const group = new LayerTreeGroupState(mapItemState, this);
// 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.expanded.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.symbology.changed');
group.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
group.addListener(this.dispatch.bind(this), 'layer.expanded.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 if (mapItemState instanceof MapLayerState) {
if (!mapItemState.displayInLayerTree) {
continue;
}
// Build layer
const layer = new LayerTreeLayerState(mapItemState, this)
layer.addListener(this.dispatch.bind(this), 'layer.visibility.changed');
layer.addListener(this.dispatch.bind(this), 'layer.expanded.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);
}
}
}
/**
* 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 {LayerTreeItemState[]}
*/
get children() {
return [...this._items];
}
/**
* Iterate through children items
* @generator
* @yields {LayerTreeItemState} The next child item
*/
*getChildren() {
for (const item of this._items) {
yield item;
}
}
/**
* Propagate throught tree item the new checked state
* @param {boolean} val The new checked state
* @returns {boolean} the new checked state
*/
propagateCheckedState(val) {
for (const item of this._items) {
if (item.type == 'group') {
item.propagateCheckedState(val);
} else {
item.checked = val;
}
if (item.checked && this.mutuallyExclusive) {
break;
}
}
this.checked = val;
return this.checked;
}
/**
* Find layer names
* @returns {string[]} List of layer names
*/
findTreeLayerNames() {
let names = []
for(const item of this.getChildren()) {
if (item instanceof LayerTreeLayerState) {
names.push(item.name);
} else if (item instanceof LayerTreeGroupState) {
names = names.concat(item.findTreeLayerNames());
}
}
return names;
}
/**
* Find layer items
* @returns {LayerTreeLayerState[]} List of tree layers (not tree groups)
*/
findTreeLayers() {
let items = []
for(const item of this.getChildren()) {
if (item instanceof LayerTreeLayerState) {
items.push(item);
} else if (item instanceof LayerTreeGroupState) {
items = items.concat(item.findTreeLayers());
}
}
return items;
}
/**
* Find layer and group items
* @returns {LayerTreeLayerState[]} List of tree layers and tree groups
*/
findTreeLayersAndGroups() {
let items = []
for(const item of this.getChildren()) {
if (item instanceof LayerTreeLayerState) {
items.push(item);
} else if (item instanceof LayerTreeGroupState) {
items.push(item);
items = items.concat(item.findTreeLayersAndGroups());
}
}
return items;
}
/**
* Get tree layer item by its name
* @param {string} name - the layer name
* @returns {LayerTreeLayerState} The LayerTreeLayerState associated to the name
*/
getTreeLayerByName(name) {
for (const layer of this.findTreeLayers()) {
if(layer.name === name) {
return layer;
}
}
throw RangeError('The layer name `'+ name +'` is unknown!');
}
}
/**
* Class representing a layer tree layer
* @class
* @augments LayerTreeItemState
*/
export class LayerTreeLayerState extends LayerTreeItemState {
/**
* Instantiate a layer tree layer
* @param {MapLayerState} mapLayerState - the map layer state
* @param {LayerTreeGroupState} [parentGroupState] - the parent layer tree group
*/
constructor(mapLayerState, parentGroupState) {
super(mapLayerState, parentGroupState);
// set default icon
this._icon = getDefaultLayerIcon(this.layerConfig);
}
/**
* vector layer is loaded in a single layer ImageLayer or not
* @type {boolean}
*/
get singleWMSLayer(){
return this._mapItemState.singleWMSLayer;
}
/**
* Vector layer has selected features
* The selected features is not empty
* @type {boolean}
*/
get hasSelectedFeatures() {
return this._mapItemState.hasSelectedFeatures;
}
/**
* Vector layer is filtered
* The expression filter is not null
* @type {boolean}
*/
get isFiltered() {
return this._mapItemState.isFiltered;
}
/**
* The source icon of the layer
* @type {string}
*/
get icon() {
if (this._mapItemState.symbology instanceof LayerIconSymbology) {
return this.symbology.icon;
}
return this._icon;
}
/**
* WMS selected layer style name
* @type {string}
*/
get wmsSelectedStyleName() {
return this._mapItemState.wmsSelectedStyleName;
}
/**
* Update WMS selected layer style name
* based on wmsStyles list
* @param {string} styleName - the WMS layer style name to select
*/
set wmsSelectedStyleName(styleName) {
this._mapItemState.wmsSelectedStyleName = styleName;
}
/**
* WMS layer styles
* @type {LayerStyleConfig[]}
*/
get wmsStyles() {
return this._mapItemState.wmsStyles;
}
/**
* WMS layer attribution
* @type {?AttributionConfig}
*/
get wmsAttribution() {
return this._mapItemState.wmsAttribution;
}
/**
* Parameters for OGC WMS Request
* @type {object}
*/
get wmsParameters() {
return this._mapItemState.wmsParameters;
}
/**
* The layer load status
* @see MapLayerLoadStatus
* @type {string}
*/
get loadStatus() {
return this._mapItemState.loadStatus;
}
/**
* Layer symbology
* @type {?(LayerIconSymbology|LayerSymbolsSymbology|LayerGroupSymbology)}
*/
get symbology() {
return this._mapItemState.symbology;
}
/**
* Update layer symbology
* @param {(object | LayerIconSymbology | LayerSymbolsSymbology | LayerGroupSymbology)} node - The symbology node
*/
set symbology(node) {
this._mapItemState.symbology = node;
}
/**
* Children symbology count
* @type {number}
*/
get symbologyChildrenCount() {
if (this._mapItemState.symbology instanceof LayerSymbolsSymbology
|| this._mapItemState.symbology instanceof LayerGroupSymbology) {
return this._mapItemState.symbology.childrenCount;
}
return 0;
}
/**
* Children symbology
* @type {(SymbolIconSymbology[]|Array.<BaseIconSymbology|BaseSymbolsSymbology>)}
*/
get symbologyChildren() {
if (this._mapItemState.symbology instanceof LayerSymbolsSymbology
|| this._mapItemState.symbology instanceof LayerGroupSymbology) {
return this._mapItemState.symbology.children;
}
return [];
}
/**
* Iterate through children nodes
* @generator
* @yields {SymbolIconSymbology|BaseIconSymbology|BaseSymbolsSymbology} The next child node
*/
*getSymbologyChildren() {
if (this._mapItemState.symbology instanceof LayerSymbolsSymbology
|| this._mapItemState.symbology instanceof LayerGroupSymbology) {
for (const symbol of this._mapItemState.symbology.getChildren()) {
yield symbol;
}
}
}
}
/**
* Class representing a layer tree group as tree root
* @class
* @augments LayerTreeGroupState
*/
export class TreeRootState extends LayerTreeGroupState {
/**
* Instantiate a root layer tree group
* @param {MapGroupState} mapGroupState - the map layer group state
*/
constructor(mapGroupState) {
super(mapGroupState);
mapGroupState.addListener(
evt => {
const extGroup = mapGroupState.children[0];
if (evt.name != extGroup.name)
return;
const extTreeGroup = new ExternalLayerTreeGroupState(extGroup);
this._items.unshift(extTreeGroup);
extTreeGroup.addListener(this.dispatch.bind(this), 'ol-layer.added');
extTreeGroup.addListener(this.dispatch.bind(this), 'ol-layer.removed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ext-group.expanded.changed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ext-group.wmsTitle.changed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ext-group.visibility.changed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ol-layer.wmsTitle.changed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ol-layer.icon.changed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ol-layer.opacity.changed');
extTreeGroup.addListener(this.dispatch.bind(this), 'ol-layer.visibility.changed');
}, ['ext-group.added']
);
mapGroupState.addListener(
evt => {
const groups = this._items
.map((item, index) => {return {'name': item.name, 'type': item.type,'index':index}})
.filter((item) => item.type == 'ext-group' && item.name == evt.name);
if (groups.length == 0) {
return;
}
const extTreeGroup = this._items.at(groups[0].index);
this._items.splice(groups[0].index, 1);
extTreeGroup.removeListener(this.dispatch.bind(this), 'ol-layer.added');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ol-layer.removed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ext-group.expanded.changed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ext-group.wmsTitle.changed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ext-group.visibility.changed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ol-layer.wmsTitle.changed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ol-layer.icon.changed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ol-layer.opacity.changed');
extTreeGroup.removeListener(this.dispatch.bind(this), 'ol-layer.visibility.changed');
}, ['ext-group.removed']
);
mapGroupState.addListener(this.dispatch.bind(this), 'ext-group.added');
mapGroupState.addListener(this.dispatch.bind(this), 'ext-group.removed');
}
}