UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

422 lines (421 loc) 17.1 kB
// SPDX-License-Identifier: Apache-2.0 import GirafeSingleton from '../../base/GirafeSingleton.js'; import CustomTheme from '../../models/customtheme.js'; import GroupLayer from '../../models/layers/grouplayer.js'; import Layer from '../../models/layers/layer.js'; import LayerWms from '../../models/layers/layerwms.js'; import ThemeLayer from '../../models/layers/themelayer.js'; import { isTimeAwareLayer } from '../../models/layers/timeawarelayer.js'; import WfsFilterCondition, { isWfsOperator } from '../wfs/wfsfiltercondition.js'; import { KNOWN_FUNCTIONALITIES } from '../functionalities.js'; import WfsFilter from '../wfs/wfsfilter.js'; export default class ThemesHelper extends GirafeSingleton { initializeSingleton() { this.context.stateManager.subscribe('themes.lastSelectedTheme', (_oldTheme, newTheme) => this.onSelectedThemeChanged(newTheme)); } get state() { return this.context.stateManager.state; } findBaseLayerById(layerId) { for (const theme of Object.values(this.state.themes._allThemes)) { if (theme.id === layerId) { return theme; } const child = this.findBaseLayerRecursiveById(theme.children, layerId); if (child) { return child; } } throw new Error(`No BaseLayer with ID ${layerId} could be found.`); } findBaseLayerRecursiveById(layers, layerId) { for (const layer of layers) { if (layer.id === layerId) { return layer; } if (layer instanceof GroupLayer || layer instanceof ThemeLayer) { const child = this.findBaseLayerRecursiveById(layer.children, layerId); if (child) { return child; } } } return null; } findThemeByName(themename) { for (const theme of Object.values(this.state.themes._allThemes)) { if (theme.name === themename) { return theme; } } console.warn(`Layer ${themename} was found, but is not a group`); return null; } findGroupByName(groupname) { const group = this.findBaseLayerByName(groupname); if (group instanceof GroupLayer) { return group; } console.warn(`Layer ${groupname} was found, but is not a group`); return null; } findLayerByName(layername) { const layer = this.findBaseLayerByName(layername); if (layer instanceof Layer) { return layer; } if (layer !== null) { console.warn(`Layer ${layername} was found, but is not a layer`); } return null; } findBaseLayerByName(layername) { for (const theme of Object.values(this.state.themes._allThemes)) { const layer = this.findLayerRecursive(theme.children, layername); if (layer) { return layer; } } console.warn(`Layer ${layername} not found!`); return null; } findLayerRecursive(layers, layername) { for (const layer of layers) { if (layer.name === layername) { return layer; } if (layer instanceof GroupLayer || layer instanceof ThemeLayer) { const child = this.findLayerRecursive(layer.children, layername); if (child) { return child; } } } return null; } onSelectedThemeChanged(theme) { if (!theme) { // Theme is null, nothing to do here return; } if (theme instanceof CustomTheme) { theme.layers.sort((a, b) => a.order - b.order); for (const t of theme.layers) { this.onThemeChanged(t); } } else { this.onThemeChanged(theme); } } onThemeChanged(theme) { // Create a clone of the theme object to use it in the treeview. // This is essential, otherwise all changes are done in the layers // (For example, when expanding legend, expanding a group, or activating the layer). // Will also be done in the default layer configuration that has been loaded from themes.json, // And when a theme is selected aging from the themes-selector // The default configuration will have been overwritten. const clonedTheme = theme.clone(); const themeAlreadyInLayersList = this.state.layers.layersList.find((l) => l.id == clonedTheme.id); if (themeAlreadyInLayersList) { themeAlreadyInLayersList.isHighlighted = true; const layername = this.context.i18nManager.getTranslation(clonedTheme.name); const msg = this.context.i18nManager .getTranslation('The theme {name} is already present in the treeview.') .replace('{name}', layername); this.state.infobox.elements.push({ id: theme.treeItemId, text: msg, type: 'info', duration: 5000 }); return; } if (this.context.configManager.Config.themes.selectionMode === 'replace') { // Mode is <replace> this.emptyLayerTree(); this.state.layers.layersList.push(clonedTheme); } else { // Mode is <add> clonedTheme.order = this.getInitialOrderForNewTheme(); this.state.layers.layersList.push(clonedTheme); } const themeFunctionalities = this.state.themes._allFunctionalities[theme.id]; if (themeFunctionalities) { for (const functionality of Object.keys(themeFunctionalities)) { if (KNOWN_FUNCTIONALITIES.includes(functionality)) { this.state.functionalities[functionality] = themeFunctionalities[functionality]; } else { console.warn(`Unknown functionality '${functionality}' found on Theme '${theme.name}'.`); } } } } getMinimalClonedThemeForLayer(layer) { const hierarchy = this.getHierarchyFromLayer(layer); const theme = hierarchy[0]; const clone = theme.clone(); // Remove unnecessary clones let children = clone.children; for (let i = 1; i < hierarchy.length; ++i) { this.removeOthers(children, hierarchy[i]); children[0].isExpanded = true; children = children[0].children; } const clonedLayer = this.findBaseLayerRecursiveById([clone], layer.id); if (clonedLayer) { clonedLayer.isHighlighted = true; } return clone; } removeOthers(layers, keep) { for (let i = layers.length - 1; i >= 0; --i) { if (layers[i].id !== keep.id) { layers.splice(i, 1); } } } getHierarchyFromLayer(layer) { if (layer instanceof ThemeLayer) { return [layer]; } if (!layer.parent) { throw new Error('A group or a layer should always have a parent.'); } const parents = this.getHierarchyFromLayer(layer.parent); return [...parents, layer]; } addThemesFromUrl() { let themeAdded = false; if (this.context.permalinkManager.hasThemes()) { for (const themename of this.context.permalinkManager.getThemes()) { const theme = Object.values(this.state.themes._allThemes).find((t) => t.name === themename); if (theme) { this.state.themes.lastSelectedTheme = theme; themeAdded = true; } else { console.warn(`Theme ${themename} cannot be found`); } } } return themeAdded; } addGroupsFromUrl() { let added = false; if (this.context.permalinkManager.hasGroups()) { for (const groupname of this.context.permalinkManager.getGroups()) { added = this.addLayerBaseFromName(groupname, 'group') || added; } } return added; } addLayersFromUrl() { let added = false; if (this.context.permalinkManager.hasLayers()) { for (const layername of this.context.permalinkManager.getLayers()) { added = this.addLayerBaseFromName(layername, 'layer') || added; } } return added; } addLayerFromName(layername) { return this.addLayerBaseFromName(layername, 'layer'); } addLayerBaseFromName(layer, type) { const layerOptions = this.extractLayerOptions(layer, type); if (!layerOptions) { console.warn(`Layer ${layer} cannot be found`); return false; } const clonedTheme = this.getMinimalClonedThemeForLayer(layerOptions.originalLayer); this.mergeLayerWithExistingLayerTree(clonedTheme, this.state.layers.layersList); if (!layerOptions.active) { // Layer added, but inactive. No further config needed return true; } const clonedLayer = this.findLayerRecursive(this.state.layers.layersList, layerOptions.originalLayer.name); if (clonedLayer) { this.context.layerManager.toggle(clonedLayer, 'on'); if (layerOptions.opacity) { clonedLayer.opacity = layerOptions.opacity; } if (layerOptions.filter) { clonedLayer.filter = layerOptions.filter; } if (layerOptions.timeRestriction) { clonedLayer.timeRestriction = layerOptions.timeRestriction; } } return true; } extractLayerOptions(urlParam, type) { let active = true; if (urlParam.startsWith('!')) { active = false; urlParam = urlParam.substring(1); } const layerOptions = urlParam.split('|'); const layername = layerOptions[0]; const layerOrGroup = type === 'layer' ? this.findLayerByName(layername) : this.findGroupByName(layername); if (!layerOrGroup) { // No layer found return; } let opacity = undefined; let filter = undefined; let timeRestriction = undefined; for (let i = 1; i < layerOptions.length; ++i) { const option = layerOptions[i]; if (option.startsWith('o;')) { opacity = this.extractLayerOptionOpacity(layerOrGroup, option); } else if (option.startsWith('f;')) { filter = this.extractLayerOptionFilter(layerOrGroup, option); } else if (option.startsWith('t;')) { timeRestriction = this.extractLayerOptionTime(layerOrGroup, option); } } return { originalLayer: layerOrGroup, active: active, opacity: opacity, filter: filter, timeRestriction: timeRestriction }; } extractLayerOptionOpacity(layerOrGroup, option) { if (!(layerOrGroup instanceof Layer)) { console.warn('Permalink: opacity configuration is only allowed for layers.'); return; } return Number(option.substring(2)); } extractLayerOptionFilter(layerOrGroup, option) { if (!(layerOrGroup instanceof Layer)) { console.warn('Permalink: filter configuration is only allowed for layers.'); return; } if (!(layerOrGroup instanceof LayerWms)) { console.warn('Permalink: filter configuration is only allowed for layers that support filters.'); return; } const filterParams = option.substring(2).split(';'); const filterProperty = filterParams[0]; const filterOperator = filterParams[1]; if (!isWfsOperator(filterOperator)) { console.warn('Permalink: filter operator is unknown.'); return; } const filterValue = filterParams[2]; const filterPropertyType = filterParams.length > 3 ? filterParams[3] : 'string'; // Note: Only one condition is supported at the moment, no chained or nested conditions return new WfsFilter([ new WfsFilterCondition(filterProperty, filterOperator, filterValue, undefined, filterPropertyType) ]); } extractLayerOptionTime(layerOrGroup, option) { if (!isTimeAwareLayer(layerOrGroup)) { console.warn('Permalink: time configuration is only allowed for layers that support time configuration.'); return; } return option.substring(2); } /** * Calculates the initial order (=position) for a new theme in the tree. * Themes are added at the top of the tree, but underneath any pinned themes. Defaults to 0. * This prevents themes of visibly "jumping" around in the tree after being added due to reordering. */ getInitialOrderForNewTheme() { return this.state.layers.layersList .filter((l) => l.isPinned) .map((l) => l.order) .reduce((a, b) => Math.max(a, b), 0); } mergeThemeInLayerTree(theme, activate = false, forceTop = false) { theme.order = forceTop ? -1 : this.getInitialOrderForNewTheme(); const layerTreeChanges = { insertedLayers: [], activatedLayers: [] }; this.mergeLayerWithExistingLayerTree(theme, this.state.layers.layersList, activate, layerTreeChanges); return layerTreeChanges; } /** * This function merges the newLayer at its right place in the LayerTree * @param newLayer The layer to insert somewhere in the hierarchy * @param existingList * @param parent * @returns */ mergeLayerWithExistingLayerTree(newLayer, existingList, activate = false, layerTreeChanges, parent) { const existingLayer = existingList.find((l) => l.id === newLayer.id); if (!existingLayer) { // The theme is not already present. We just add the theme to the layertree this.addLayerToLayerTree(newLayer, existingList, activate, layerTreeChanges, parent); return; } if (newLayer.isHighlighted) { // The layer is already present this.highlightLayerInLayerTree(existingLayer, activate, layerTreeChanges); return; } // Otherwise, we have to merge the themes/groups if ((newLayer instanceof ThemeLayer && existingLayer instanceof ThemeLayer) || (newLayer instanceof GroupLayer && existingLayer instanceof GroupLayer)) { for (const child of newLayer.children) { this.mergeLayerWithExistingLayerTree(child, existingLayer.children, activate, layerTreeChanges, existingLayer); } } } highlightLayerInLayerTree(existingLayer, activate, layerTreeChanges) { existingLayer.isHighlighted = true; if (existingLayer.parent) { existingLayer.parent.isExpanded = true; } if (activate && existingLayer.inactive) { this.context.layerManager.toggle(existingLayer, 'on'); if (layerTreeChanges) { layerTreeChanges.activatedLayers.push(existingLayer); } } } addLayerToLayerTree(newLayer, existingList, activate = false, layerTreeChanges, parent) { if (parent) { newLayer.parent = parent; } existingList.push(newLayer); if (activate) { this.context.layerManager.toggle(newLayer, 'on'); } if (layerTreeChanges) { layerTreeChanges.insertedLayers.push(newLayer); } } removeLayersFromLayerTree(layersToRemove) { for (const layerToRemove of layersToRemove) { this.context.layerManager.toggle(layerToRemove, 'off'); this.removeLayersFromExistingLayerTree(layerToRemove, this.state.layers.layersList); } } removeLayersFromExistingLayerTree(layerToRemove, existingList) { const existingLayerIndex = existingList.findIndex((l) => l.id === layerToRemove.id); if (existingLayerIndex >= 0) { existingList.splice(existingLayerIndex, 1); } else { for (const element of existingList) { if (element instanceof ThemeLayer || element instanceof GroupLayer) { this.removeLayersFromExistingLayerTree(layerToRemove, element.children); } } } } /** * Removes all layers from the layer tree except themes that are pinned. */ emptyLayerTree() { const layersToRemove = this.state.layers.layersList.filter((l) => !l.isPinned); this.context.stateManager.batchChanges(() => this.removeLayersFromLayerTree(layersToRemove)); } }