@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
379 lines (378 loc) • 17.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import { v4 as uuidv4 } from 'uuid';
import GirafeSingleton from '../../base/GirafeSingleton.js';
import Basemap from '../../models/basemaps/basemap.js';
import GroupLayer from '../../models/layers/grouplayer.js';
import LayerOsm from '../../models/layers/layerosm.js';
import LayerVectorTiles from '../../models/layers/layervectortiles.js';
import LayerWmts from '../../models/layers/layerwmts.js';
import LayerWms from '../../models/layers/layerwms.js';
import LayerCog from '../../models/layers/layercog.js';
import LayerXYZ from '../../models/layers/layerxyz.js';
import ServerOgc from '../../models/serverogc.js';
import ThemeLayer from '../../models/layers/themelayer.js';
import BasemapEmpty from '../../models/basemaps/basemapempty.js';
import BasemapSwisstopoVectorTiles from '../../models/basemaps/basemapswisstopovectortiles.js';
import BasemapOsm from '../../models/basemaps/basemaposm.js';
import { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes-config.js';
import Layer from '../../models/layers/layer.js';
import { applyDefaultPrefixToUrl } from '../utils/utils.js';
class ThemesManager extends GirafeSingleton {
anonymousUserInfo = { u: 'anonymous' };
get state() {
return this.context.stateManager.state;
}
initializeSingleton() {
// We have to wait the authentication to be able to load the themes with the right user-rights
this.context.stateManager.subscribe('application.isReadyToLoadThemes', async () => {
if (this.state.application.isReadyToLoadThemes) {
await this.initialize();
}
});
// This is for the next change in the oAuth status.
this.context.stateManager.subscribe('oauth.status', async () => {
if (this.state.themes.isLoaded &&
((this.state.oauth.somethingChanged && this.state.oauth.status === 'loggedIn') ||
this.state.oauth.status === 'loggedOut')) {
await this.initialize();
}
});
}
async initialize() {
try {
await this.loadThemes();
console.log('Themes were loaded');
const stateRestored = await this.restoreState();
const themeAddedFromUrl = this.context.themesHelper.addThemesFromUrl();
const groupAddedFromUrl = this.context.themesHelper.addGroupsFromUrl();
const layersAddedFromUrl = this.context.themesHelper.addLayersFromUrl();
const basemapAddedFromUrl = this.addBasemapFromUrl();
if (!stateRestored && !themeAddedFromUrl && !groupAddedFromUrl && !layersAddedFromUrl) {
this.setDefaultTheme();
}
if (!stateRestored && !basemapAddedFromUrl) {
this.setDefaultBasemap();
}
this.context.stateManager.state.application.isStateInitialized = true;
this.context.sessionManager.beginSession();
}
catch (error) {
// Themes could not be loaded
console.error(error);
await window.gAlert('An error occurred while loading the themes. This has nothing to do with GeoGirafe and is very probably a backend-configuration error (wrong content in themes.json, or CORS error). Please check the backend configuration.', 'Backend error');
await window.gAlert('This instance of GeoGirafe cannot be used at the moment.', 'Backend error');
console.error(error);
}
}
async restoreState() {
// try to restore state if any
let stateRestored = false;
if (this.context.shareManager.hasSharedState()) {
stateRestored = await this.context.shareManager.setStateFromUrl();
}
else if (this.context.sessionManager.hasState()) {
stateRestored = this.context.sessionManager.setStateFromSession();
}
return stateRestored;
}
addBasemapFromUrl() {
if (this.context.permalinkManager.hasBasemap()) {
const bs = this.context.permalinkManager.getBasemap();
for (const basemap of Object.values(this.state.basemaps)) {
if (basemap.name === bs) {
this.state.activeBasemaps = [basemap];
return true;
}
}
// Nothing found
console.warn(`Basemap ${bs} cannot be found`);
}
return false;
}
/**
* Load themes from backend and configures background layers if needed
*/
async loadThemes() {
this.state.themes.isLoaded = false;
const response = await fetch(this.context.configManager.Config.themes.url);
const content = await response.json();
this.state.ogcServers = this.prepareOgcServers(content['ogcServers']);
this.state.basemaps = this.prepareBasemaps(content['background_layers']);
const themesAndFunctionalities = this.prepareThemes(content['themes']);
this.state.themes._allThemes = themesAndFunctionalities.themes;
this.state.themes._allFunctionalities = themesAndFunctionalities.themesFunctionalities;
this.context.customThemesManager.loadCustomThemes();
this.state.themes.isLoaded = true;
if (this.context.configManager.Config.themes.showErrorsOnStart) {
// Display themes errors only if configured so.
// Parse errors if any
for (const error of content['errors']) {
this.context.errorManager.pushMessage(uuidv4(), error, 'error');
}
}
}
setDefaultTheme() {
// Set default theme if any
if (!this.isNullOrUndefinedOrBlank(this.context.configManager.Config.themes.defaultTheme)) {
const themes = [
...Object.values(this.state.themes._allThemes),
...Object.values(this.context.customThemesManager.customThemes)
];
const defaultTheme = themes.find((t) => t.name === this.context.configManager.Config.themes.defaultTheme);
if (defaultTheme) {
this.state.themes.lastSelectedTheme = defaultTheme;
}
else {
// The default theme was not found
console.warn(`The default theme ${this.context.configManager.Config.themes.defaultTheme} could not be found.`);
}
}
}
setDefaultBasemap() {
for (const basemap of Object.values(this.state.basemaps)) {
if (basemap.name === this.context.configManager.Config.basemaps.defaultBasemap) {
this.state.activeBasemaps = [basemap];
break;
}
}
}
prepareOgcServers(ogcServerJson) {
const servers = {};
if (ogcServerJson) {
for (const serverName of Object.keys(ogcServerJson)) {
const server = new ServerOgc(serverName, ogcServerJson[serverName]);
servers[serverName] = server;
}
// Preload WFS FeatureInfos
this.preloadWfsServer(servers);
}
return servers;
}
/**
* Preload all WFS DescribeFeatureType
* In order to limit the network overload, the servers calls are done sequetially
* and each call will wait the previous one to be done
*/
async preloadWfsServer(ogcServers) {
for (const server of Object.values(ogcServers)) {
if (server.wfsSupport) {
await this.context.wfsManager.getServerWfs(server);
}
}
}
prepareBasemaps(basemapJson) {
const basemaps = {};
if (this.context.configManager.Config.basemaps.emptyBasemap) {
const basemapEmpty = new BasemapEmpty();
basemaps[basemapEmpty.id] = basemapEmpty;
}
if (this.context.configManager.Config.basemaps.OSM) {
const basemapOsm = new BasemapOsm();
basemaps[basemapOsm.id] = basemapOsm;
}
if (this.context.configManager.Config.basemaps.SwissTopoVectorTiles) {
const basemapSwisstopoVectorTiles = new BasemapSwisstopoVectorTiles();
basemaps[basemapSwisstopoVectorTiles.id] = basemapSwisstopoVectorTiles;
}
this.addBasemapsFromConfig(basemapJson, basemaps);
this.applyOpacityToBasemaps(basemaps);
return basemaps;
}
addBasemapsFromConfig(basemapsJson, basemaps) {
basemapsJson.forEach((elem) => {
// Create basemap
const basemap = new Basemap(elem);
basemaps[basemap.id] = basemap;
// List all layers in this basemap
const order = { value: 0 };
if (elem.children) {
// Multiple layers
elem.children.forEach((child) => {
const layer = this.prepareThemeLayer(child, null, order);
if (layer) {
basemap.layersList.push(layer);
}
});
}
else {
// Only one layer in this basemap
const layer = this.prepareThemeLayer(elem, null, order);
if (layer) {
basemap.layersList.push(layer);
}
}
});
}
applyOpacityToBasemaps(basemaps) {
for (const basemap of Object.values(basemaps)) {
if (this.context.configManager.Config.basemaps.opacityBasemaps.includes(basemap.name)) {
this.applyOpacityToBasemap(basemap);
}
}
}
applyOpacityToBasemap(basemap) {
// If it is the default Basemap the Opacity should NOT be 0 as otherwise the User would end up seeing nothing
const isDefaultBasemap = this.context.configManager.Config.basemaps.defaultBasemap == basemap.name;
basemap.opacity = isDefaultBasemap ? OPACITY_FOR_DEFAULT_BASEMAP : DEFAULT_OPACITY;
for (const basemapLayer of basemap.layersList) {
if (basemapLayer instanceof LayerVectorTiles) {
// Vector tiles layers are not supported as opacitybasemap, because the tiles cannot reprojected on the fly
// And displaying a basemap from some SRID with an VT from another SRID won't work
// So for the moment we do not allow VT configured as opacitybasemaps
// (But the opposite will still work : a VT basemap with a WMTS opacitybasemap)
}
else if (basemapLayer instanceof Layer) {
basemapLayer.opacity = basemap.opacity;
}
}
}
prepareThemes(themesJson) {
const themes = {};
const order = { value: 0 };
const themesFunctionalities = {};
themesJson.forEach((themeJson, index) => {
if (!themeJson.icon.startsWith('http') && this.context.configManager.Config.themes.imagesUrlPrefix) {
themeJson.icon = this.context.configManager.Config.themes.imagesUrlPrefix + themeJson.icon;
}
themesFunctionalities[themeJson.id] = themeJson['functionalities'];
const theme = new ThemeLayer(themeJson['id'], themeJson['name'], index, themeJson['icon'], themeJson['metadata']);
themeJson.children.forEach((layerJson) => {
const layer = this.prepareThemeLayer(layerJson, null, order);
if (layer) {
layer.parent = theme;
theme.children.push(layer);
}
});
themes[index] = theme;
});
return { themes, themesFunctionalities };
}
/**
* Will create layer and child layers if elem passed is a group of layers
* @param elem either a layer or a group of layers
* @param parentServer in case children are not mixed layers, the parentServer will apply for all children
* @param order the order in the layer list
* @returns the created girafe layer
*/
prepareThemeLayer(elem, parentServer, order) {
// If a server is defined on this node, we use it.
// Otherwise, we use the server of the parent
const ogcServerName = elem.ogcServer ? elem.ogcServer : parentServer;
// Create Layer
let layer;
switch (elem.type) {
case 'OSM': {
layer = new LayerOsm(order.value);
break;
}
case 'VectorTiles': {
layer = this.createVectorTilesLayer(elem, order);
break;
}
case 'WMTS': {
layer = this.createWmtsLayer(elem, order);
break;
}
case 'WMS': {
layer = this.createWmsLayer(ogcServerName, elem, order);
break;
}
case 'COG': {
layer = new LayerCog(elem.id, elem.name, order.value, elem.url, elem);
break;
}
case 'XYZ': {
layer = new LayerXYZ(elem.id, elem.name, order.value, elem.url, elem);
break;
}
default: {
// Group
layer = this.createGroup(elem, order, ogcServerName);
}
}
order.value = order.value + 1;
return layer;
}
createGroup(elem, order, ogcServerName) {
const options = {
isDefaultChecked: elem.metadata?.isChecked,
metadataUrl: elem.metadata?.metadataUrl,
disclaimer: elem.metadata?.disclaimer,
isDefaultExpanded: elem.metadata?.isExpanded,
isExclusiveGroup: elem.metadata?.exclusiveGroup,
isMixed: elem.mixed,
time: elem.time
};
if (options.metadataUrl) {
options.metadataUrl = applyDefaultPrefixToUrl(this.context, options.metadataUrl);
}
const group = new GroupLayer(elem.id, elem.name, order.value, options);
// Append children
if (elem.children) {
for (const child of elem.children) {
// Append time options to child
if (options.time) {
child.time = { ...options.time };
}
const childLayer = this.prepareThemeLayer(child, ogcServerName, order);
if (childLayer) {
childLayer.parent = group;
group.children.push(childLayer);
}
}
}
return group;
}
createWmsLayer(ogcServerName, elem, order) {
if (ogcServerName) {
const ogcServer = this.state.ogcServers[ogcServerName];
const options = elem;
if (options.metadata?.metadataUrl) {
options.metadata.metadataUrl = applyDefaultPrefixToUrl(this.context, options.metadata.metadataUrl);
}
const wmsLayer = new LayerWms(elem.id, elem.name, order.value, ogcServer, elem);
// For WMFS queries, the layers attribute will be used. It can be different from the name.
// But in order to be able to define the translations only once, we use an translation alias here.
if (wmsLayer.layers) {
this.context.i18nManager.addTranslationAlias(wmsLayer.name, wmsLayer.layers);
}
return wmsLayer;
}
else {
// Layer is invalid : it does not have any OGC-Server
this.context.errorManager.pushMessage(uuidv4(), `Layer ${elem.name} (id=${elem.id}) is invalid and cannot be created: missing OGC-Server.`, 'error');
}
return null;
}
createWmtsLayer(elem, order) {
const ogcServer = elem.metadata?.ogcServer ? this.state.ogcServers[elem.metadata?.ogcServer] : undefined;
const options = elem;
if (options.metadata?.metadataUrl) {
options.metadata.metadataUrl = applyDefaultPrefixToUrl(this.context, options.metadata.metadataUrl);
}
return new LayerWmts(elem.id, elem.name, order.value, elem.url, elem.layer, options, ogcServer);
}
createVectorTilesLayer(elem, order) {
if (!elem.style || !elem.metadata?.layerName) {
// Layer is invalid : it must contain style URL and layername
this.context.errorManager.pushMessage(uuidv4(), `VectorTiles-Layer ${elem.name} (id=${elem.id}) is invalid and cannot be created: missing Style Url or layername.`, 'error');
}
else {
const options = {
// TODO REG : At the moment projection is hardcoded to EPSG:3857 because we don't have any other usecase.
// But the supported EPSG should be configurable in the backend
projection: 'EPSG:3857',
isDefaultChecked: elem.metadata?.isChecked,
disclaimer: elem.metadata?.disclaimer,
metadata: elem.metadata,
layerName: elem.metadata.layerName
};
if (options.metadata?.metadataUrl) {
options.metadata.metadataUrl = applyDefaultPrefixToUrl(this.context, options.metadata.metadataUrl);
}
return new LayerVectorTiles(elem.id, elem.name, order.value, elem.style, options);
}
return null;
}
}
export default ThemesManager;