@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
422 lines (421 loc) • 17.1 kB
JavaScript
// 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));
}
}