UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

1,143 lines 67 kB
import { html as uHtml } from 'uhtml';
// SPDX-License-Identifier: Apache-2.0
import { Collection, Feature } from 'ol';
import { Circle, Fill, Icon, RegularShape, Stroke, Style } from 'ol/style.js';
import VectorSource from 'ol/source/Vector.js';
import VectorLayer from 'ol/layer/Vector.js';
import { DragBox } from 'ol/interaction.js';
import { ScaleLine } from 'ol/control.js';
import { GeometryCollection, Point } from 'ol/geom.js';
import { getCenter, getHeight, getWidth } from 'ol/extent.js';
import { Cesium3DTileset } from 'cesium';
import proj4 from 'proj4';
import SwipeManager from './tools/swipemanager.js';
import WmsManager3d from './tools/wmsmanager3d.js';
import OsmManager from './tools/osmmanager.js';
import VectorTilesManager from './tools/vectortilesmanager.js';
import WmtsManager from './tools/wmtsmanager.js';
import ViewManager from './tools/viewmanager.js';
import CogManager from './tools/cogmanager.js';
import DrawingManager from './tools/drawingmanager.js';
import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
import Layer from '../../models/layers/layer.js';
import LayerCog from '../../models/layers/layercog.js';
import LayerXYZ from '../../models/layers/layerxyz.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 LayerLocalFile from '../../models/layers/layerlocalfile.js';
import LayerDrawing from '../../models/layers/layerdrawing.js';
import GeoEvents from '../../models/events.js';
import GroupLayer from '../../models/layers/grouplayer.js';
import { FocusFeature } from './tools/focusfeature.js';
import XyzManager from './tools/xyzmanager.js';
import ThemeLayer from '../../models/layers/themelayer.js';
import { isTimeAwareLayer } from '../../models/layers/timeawarelayer.js';
import { debounce } from '../../tools/utils/debounce.js';
import CircleStyle from 'ol/style/Circle.js';
import { parseCoordinates } from '../../tools/geometrytools.js';
import CircleGeom from 'ol/geom/Circle.js';
import WfsFilter from '../../tools/wfs/wfsfilter.js';
import WfsFilterCondition from '../../tools/wfs/wfsfiltercondition.js';
import { applyFeaturesToSelection, applyOpacityToLayers } from '../../tools/utils/utils.js';
import { SelectionMode } from '../../models/selection.js';
import { platformModifierKeyOnly } from 'ol/events/condition.js';
import { HighlightMode } from '../../tools/configuration/girafeconfig.js';
export default class MapComponent extends GirafeHTMLElement {
    templateUrl = null;
    styleUrls = null;
    template = () => {
        return uHtml `<style>
:root,:host{--ol-background-color:white;--ol-accent-background-color:#f5f5f5;--ol-subtle-background-color:#80808040;--ol-partial-background-color:#ffffffbf;--ol-foreground-color:#333;--ol-subtle-foreground-color:#666;--ol-brand-color:#0af}.ol-box{box-sizing:border-box;border:1.5px solid var(--ol-background-color);background-color:var(--ol-partial-background-color);border-radius:2px}.ol-mouse-position{position:absolute;top:8px;right:8px}.ol-scale-line{background:var(--ol-partial-background-color);border-radius:4px;padding:2px;position:absolute;bottom:8px;left:8px}.ol-scale-line-inner{border:1px solid var(--ol-subtle-foreground-color);color:var(--ol-foreground-color);text-align:center;will-change:contents, width;border-top:none;margin:1px;font-size:10px;transition:all .25s}.ol-scale-bar{position:absolute;bottom:8px;left:8px}.ol-scale-bar-inner{display:flex}.ol-scale-step-marker{background-color:var(--ol-foreground-color);float:right;z-index:10;width:1px;height:15px}.ol-scale-step-text{z-index:11;color:var(--ol-foreground-color);text-shadow:-1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);font-size:10px;position:absolute;bottom:-5px}.ol-scale-text{text-align:center;color:var(--ol-foreground-color);text-shadow:-1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);font-size:12px;position:absolute;bottom:25px}.ol-scale-singlebar{z-index:9;box-sizing:border-box;border:1px solid var(--ol-foreground-color);height:10px;position:relative}.ol-scale-singlebar-even{background-color:var(--ol-subtle-foreground-color)}.ol-scale-singlebar-odd{background-color:var(--ol-background-color)}.ol-unsupported{display:none}.ol-viewport,.ol-unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.ol-viewport canvas{all:unset;overflow:hidden}.ol-viewport{touch-action:pan-x pan-y}.ol-selectable{-webkit-touch-callout:default;-webkit-user-select:text;-moz-user-select:text;user-select:text}.ol-grabbing{cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.ol-grab{cursor:move;cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.ol-control{background-color:var(--ol-subtle-background-color);border-radius:4px;position:absolute}.ol-zoom{top:.5em;left:.5em}.ol-rotate{transition:opacity .25s linear,visibility linear;top:.5em;right:.5em}.ol-rotate.ol-hidden{opacity:0;visibility:hidden;transition:opacity .25s linear,visibility 0s linear .25s}.ol-zoom-extent{top:4.643em;left:.5em}.ol-full-screen{top:.5em;right:.5em}.ol-control button{color:var(--ol-subtle-foreground-color);font-weight:700;font-size:inherit;text-align:center;background-color:var(--ol-background-color);border:none;border-radius:2px;width:1.375em;height:1.375em;margin:1px;padding:0;line-height:.4em;text-decoration:none;display:block}.ol-control button::-moz-focus-inner{border:none;padding:0}.ol-zoom-extent button{line-height:1.4em}.ol-compass{will-change:transform;font-weight:400;display:block}.ol-touch .ol-control button{font-size:1.5em}.ol-touch .ol-zoom-extent{top:5.5em}.ol-control button:hover,.ol-control button:focus{outline:1px solid var(--ol-subtle-foreground-color);color:var(--ol-foreground-color);text-decoration:none}.ol-zoom .ol-zoom-in{border-radius:2px 2px 0 0}.ol-zoom .ol-zoom-out{border-radius:0 0 2px 2px}.ol-attribution{text-align:right;flex-flow:row-reverse;align-items:center;max-width:calc(100% - 1.3em);display:flex;bottom:.5em;right:.5em}.ol-attribution a{color:var(--ol-subtle-foreground-color);text-decoration:none}.ol-attribution ul{color:var(--ol-foreground-color);text-shadow:0 0 2px var(--ol-background-color);margin:0;padding:1px .5em;font-size:12px}.ol-attribution li{list-style:none;display:inline}.ol-attribution li:not(:last-child):after{content:" "}.ol-attribution img{max-height:2em;max-width:inherit;vertical-align:middle}.ol-attribution button{flex-shrink:0}.ol-attribution.ol-collapsed ul{display:none}.ol-attribution:not(.ol-collapsed){background:var(--ol-partial-background-color)}.ol-attribution.ol-uncollapsible{border-radius:4px 0 0;bottom:0;right:0}.ol-attribution.ol-uncollapsible img{max-height:1.6em;margin-top:-.2em}.ol-attribution.ol-uncollapsible button{display:none}.ol-zoomslider{height:200px;top:4.5em;left:.5em}.ol-zoomslider button{height:10px;position:relative}.ol-touch .ol-zoomslider{top:5.5em}.ol-overviewmap{bottom:.5em;left:.5em}.ol-overviewmap.ol-uncollapsible{border-radius:0 4px 0 0;bottom:0;left:0}.ol-overviewmap .ol-overviewmap-map,.ol-overviewmap button{display:block}.ol-overviewmap .ol-overviewmap-map{border:1px solid var(--ol-subtle-foreground-color);width:150px;height:150px}.ol-overviewmap:not(.ol-collapsed) button{position:absolute;bottom:0;left:0}.ol-overviewmap.ol-collapsed .ol-overviewmap-map,.ol-overviewmap.ol-uncollapsible button{display:none}.ol-overviewmap:not(.ol-collapsed){background:var(--ol-subtle-background-color)}.ol-overviewmap-box{border:1.5px dotted var(--ol-subtle-foreground-color)}.ol-overviewmap .ol-overviewmap-box:hover{cursor:move}.ol-overviewmap .ol-viewport:hover{cursor:pointer}
</style><style>
.hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a,a:visited{color:var(--link-color)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-timing-function:linear;animation-iteration-count:infinite}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}button,input,select,textarea{font:inherit}.gg-button,.gg-select,.gg-input,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);box-sizing:border-box;cursor:pointer;border-radius:3px;outline:0;margin:0;padding:0 0 0 .5rem;display:inline-block}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;align-items:center;margin:0;padding:0;display:flex}.gg-button,.gg-select,.gg-input,.gg-label{min-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{max-height:initial;resize:vertical;height:6rem;padding:.5rem;line-height:1.3rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad1);background-color:var(--bkg-color-grad2)}.gg-button:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed;border:none}.gg-button:disabled img{filter:opacity(.6)}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{background-color:var(--bkg-color-grad1);color:var(--text-color-grad2);cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{color:var(--text-color);cursor:pointer;background-color:#0000;border:none;flex-direction:column;justify-content:center;align-items:center;padding:0;display:flex}.gg-icon{justify-content:center;align-items:center;display:flex}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{width:calc(var(--app-standard-height) - 1.5rem);margin:0}.gg-big-withtext span{font-variant:small-caps;padding:0 1rem;font-size:.9rem}.gg-medium,.gg-medium-withtext{min-width:calc(var(--app-standard-height) / 1.2);min-height:calc(var(--app-standard-height) / 1.2);max-height:calc(var(--app-standard-height) / 1.2);flex-direction:row}.gg-medium img{width:calc(var(--app-standard-height) / 2.4);margin:0}.gg-medium-withtext img{width:calc(var(--app-standard-height) / 2.4);margin-left:.5rem}.gg-medium-withtext span{padding:0 1rem 0 .5rem;font-size:.9rem}.gg-small,.gg-small-withtext{min-width:calc(var(--app-standard-height) / 2);min-height:calc(var(--app-standard-height) / 2);max-height:calc(var(--app-standard-height) / 2);flex-direction:row}.gg-small img{width:calc(var(--app-standard-height) / 3);margin:0}.gg-small-withtext img{width:calc(var(--app-standard-height) / 3);margin-left:.5rem}.gg-small-withtext span{padding:0 .5rem 0 .3rem;font-size:.9rem}.gg-button:hover:not(:disabled),.gg-select:hover:not(:disabled),.gg-input:hover:not(:disabled),.gg-textarea:hover:not(:disabled),.gg-icon-button:hover:not(:disabled){background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{opacity:1;background-color:#0000}.gg-tabs{cursor:pointer;grid-auto-flow:column;padding-bottom:1rem;font-size:1rem;display:grid}.gg-tab{border:none;border-bottom:var(--app-standard-border);cursor:pointer;color:var(--text-color);background:0 0;padding:.5rem}.gg-tab.active{border-bottom:solid 1px var(--text-color)}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{color:var(--text-color);background-color:#0000;border:none;flex-direction:column;display:flex}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert()}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{height:2rem;margin:.3rem}.girafe-button-large span{height:2rem;margin:.3rem;line-height:2rem}.girafe-button-small{min-width:calc(var(--app-standard-height) / 2);height:calc(var(--app-standard-height) / 2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{text-align:left;text-overflow:ellipsis;width:100%;overflow:hidden}.girafe-button-tiny{align-items:center;width:1rem;height:1rem;padding:0}.girafe-button-tiny img{overflow:hidden}.girafe-onboarding-theme{background-color:var(--bkg-color)!important;color:var(--text-color)!important}.girafe-onboarding-theme button{background-color:var(--bkg-color)!important;color:var(--text-color)!important;text-shadow:none!important}.girafe-onboarding-theme button.driver-popover-close-btn{z-index:10000}.tippy-box{background-color:var(--bkg-color)!important;border:var(--app-standard-border)!important}.tippy-content{color:var(--text-color)!important}.tippy-arrow{color:var(--bkg-color)!important}.tippy-box[data-theme~=error]{background-color:var(--error-color);white-space:pre-line}div.tippy-box[data-theme=image-popup]{border:var(--app-standard-border)!important;& div.tippy-content{background-color:var(--bkg-color)!important;& img{max-width:20rem;max-height:20rem}}& div.tippy-arrow{color:var(--bkg-color)!important}}
</style><style>
#container,#ol-map{background-color:var(--bkg-color);width:100%;height:100%;position:relative}#ol-map.darkmap canvas{filter:invert()hue-rotate(180deg)}.hidden{display:none}.center{text-align:center;margin:10px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.loading span{color:var(--text-color);margin-top:1rem;font-weight:600;display:block}.loading span.quote{font-style:italic;font-weight:300}#cs-map{background-color:var(--bkg-color);border-left:3px solid #444;height:100%;display:none;position:absolute;overflow:hidden}.ol-control{background-color:#0000;border:none;box-shadow:0 1px 4px #0000004d;right:2rem!important;left:unset!important}.ol-zoom{top:2rem!important}.mobile-map .ol-zoom{display:none!important}.ol-rotate{top:11.6rem!important}.ol-fullscreen{top:7rem!important}.ol-location{top:9.3rem!important}.img-location,.img-disable-location,.img-fullscreen{width:55%;height:auto}.ol-zoom-in,.ol-zoom-out,.btn-location,.btn-disable-location,.btn-fullscreen,.btn-exit-fullscreen,.ol-rotate-reset{cursor:pointer;background-color:var(--bkg-color)!important;width:2rem!important;height:2rem!important;color:var(--text-color)!important;font-size:1.2rem!important}.ol-scale-line{bottom:.5rem!important;right:1rem!important;left:unset!important}#swiper{width:100%;height:0;margin:0;display:none;position:absolute;top:50%}input[type=range]{-webkit-appearance:none;width:100%}input[type=range]::-webkit-slider-runnable-track{height:0}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;background:var(--bkg-color);cursor:ew-resize;border:2px solid #444;width:6px;height:0;margin-top:-1000px;padding-top:1000px;padding-bottom:1000px}input[type=range]::-moz-range-thumb{background:var(--bkg-color);cursor:ew-resize;border:2px solid #444;width:3px;height:0;padding-top:1000px;padding-bottom:1000px}.close-swiper{--button-size:2rem;transform:translateX(calc(-1 * var(--button-size) / 2));width:var(--button-size);height:var(--button-size);background-color:var(--bkg-color);color:var(--text-color);cursor:pointer;border-width:1px;border-radius:4px;display:none;position:absolute;top:0}.close-swiper:hover,.close-swiper:focus{outline:1px solid var(--text-color);color:var(--text-color-grad1)}.ol-viewport .tooltip{color:#fff;opacity:.7;white-space:nowrap;background:#00000080;border-radius:0;padding:.32rem .62rem;position:relative}@media screen and (hover:none){.ol-zoom,.ol-rotate,.ol-location,.ol-fullscreen{display:none!important}}.contextmenu{background-color:var(--bkg-color);color:var(--text-color);border:1px solid #ccc;box-shadow:1px 3px 4px #0006;& .hidden{display:none}& .menu-entry{cursor:pointer;padding:10px}& .menu-entry:hover{background-color:var(--bkg-color-grad1)}& .menu-entry[aria-disabled=true]{pointer-events:none;color:var(--text-color-grad1)}}
</style>
<style>${this.customStyle}</style>
<div id="container"><div id="ol-map" class="${this.onMobile ? 'mobile-map' : ''}"></div><div class="${this.onMobile ? 'hidden' : 'ol-fullscreen ol-unselectable ol-control'}"><button id="fullscreen-enter-button" onclick="${() => this.toggleFullscreen()}" tip="fullscreen-on" class="btn-fullscreen"><img class="img-fullscreen" alt="fullscreen-icon" src="icons/fullscreen.svg"></button> <button id="fullscreen-exit-button" onclick="${() => this.toggleFullscreen()}" tip="fullscreen-off" class="btn-exit-fullscreen hidden"><img class="img-fullscreen" alt="fullscreen-exit-icon" src="icons/fullscreen-exit.svg"></button></div><div class="${this.onMobile ? 'hidden' : 'ol-location ol-unselectable ol-control'}"><button onclick="${() => this.locateUser()}" class="btn-location" tip="Locate user"><img class="img-location" alt="location-icon" src="icons/adjust.svg"></button> <button id="disable-location" onclick="${() => this.disableLocateUser()}" tip="Disable locate user" class="btn-disable-location hidden"><img class="img-disable-location" alt="disable-location-icon" src="icons/adjust-disable.svg"></button></div><div id="cs-map"><div class="${this.loading ? 'loading center' : 'loading hidden'}"><img alt="loading-icon" src="icons/loading.svg" class="gg-spin"> <span i18n="Loading cesium...">Loading cesium...</span> <span class="quote" i18n="cesium-loading-quote">Please be patient, like a giraffe reaching for the tastiest leaves.</span></div></div><input id="swiper" type="range" min="0" max="1000" step="1"> <button tip="Hide Swiper" id="close-swiper" class="close-swiper">×</button></div>
${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
    };
    olMap;
    mapTarget;
    // TODO REG : Howto use the right type here without importing the whole library (it needs to be imported only on demand) ?
    // This works but needs the library: type OLCesiumType = typeof OLCesium;
    map3d;
    map3dTarget;
    map3dShadowsTimestamp;
    loading = false;
    swiper;
    closeSwiperButton;
    swipeManager;
    wmtsManager;
    wmsManager3d = null;
    osmManager;
    cogManager;
    xyzManager;
    drawingManager;
    viewManager;
    vectorTilesManager;
    defaultSrid;
    crosshairFeature;
    crosshairLayer;
    geolocationSource;
    markerSource = new VectorSource();
    markerLayer = new VectorLayer({
        source: this.markerSource
    });
    mapMarkerUnderCursor = null;
    previousMapCursorStyle = 'default';
    get projection() {
        return this.olMap.getView().getProjection();
    }
    get config() {
        return this.context.configManager.Config;
    }
    get onMobile() {
        return this.state.interface.isMobile;
    }
    subscribe(path, callback) {
        // @ts-expect-error The call would have succeeded against this implementation,
        // but implementation signatures of overloads are not externally visible.
        return super.subscribe(path, callback);
    }
    // For object selection
    selectedFeaturesCollection = new Collection();
    highlightedFeaturesCollection = new Collection();
    selectionLayer;
    selectionLayerStyle;
    selectionLayerStyleFunc = (feature) => {
        const shouldApplyCheck = this.config.selection.defaultHighlightMode === HighlightMode.HighlightPerLayer &&
            this.config.interface.defaultSelectionComponent === 'grid';
        if (shouldApplyCheck) {
            const selectedFeaturesType = this.state.selection.selectedFeaturesType;
            if (selectedFeaturesType && feature.getId().startsWith(selectedFeaturesType)) {
                return this.selectionLayerStyle;
            }
        }
        else {
            return this.selectionLayerStyle;
        }
    };
    highlightLayer;
    pixelTolerance = 10;
    dragbox;
    focusFeature;
    mapTargetResizeObserver;
    constructor() {
        super('map');
    }
    /**
     * The Map needs to be displayed in Fullscreen Mode.
     */
    get isFullscreenComponent() {
        return true;
    }
    resetAllSwipedLayers(layers) {
        for (const layer of layers) {
            if (layer instanceof Layer) {
                layer.swiped = 'no';
            }
            else if (layer instanceof GroupLayer || layer instanceof ThemeLayer) {
                this.resetAllSwipedLayers(layer.children);
            }
        }
        this.swipeManager.deactivateSwiper();
    }
    registerEvents() {
        this.swiper.addEventListener('input', () => {
            this.olMap.render();
            this.updateCloseSwiperPosition();
        });
        this.closeSwiperButton.onclick = () => {
            this.resetAllSwipedLayers(this.state.layers.layersList);
        };
        this.subscribe('activeBasemaps', (_, newBasemaps) => this.onChangeBasemaps(newBasemaps));
        this.subscribe(/activeBasemaps\.\d\.opacity/, (_oldOpacity, _newOpacity, basemap) => this.onChangeBasemapOpacity(basemap));
        this.subscribe('projection', (oldProjection, newProjection) => this.onChangeProjection(oldProjection, newProjection));
        this.subscribe('interface.darkMapMode', () => this.onChangeDarkMode());
        this.subscribe('interface.fullscreenMode', (_oldValue, newValue) => this.updateFullscreenButton(newValue));
        this.subscribe('selection.selectionParameters', (_, newParams) => this.onSelectFeatures(newParams));
        this.subscribe('selection.selectedFeatures', (_oldFeatures, newFeatures) => this.onFeaturesSelected(newFeatures));
        this.subscribe('selection.selectedFeaturesType', (_oldFeaturesType, newFeaturesType) => this.onFeaturesTypeChanged(newFeaturesType));
        this.subscribe('selection.highlightedFeatures', (_oldFeatures, newFeatures) => this.onFeatureHighlighted(newFeatures));
        this.subscribe('selection.focusedFeatures', (_oldFeature, newFeature) => this.focusFeature.setFocusedFeatures(newFeature));
        this.subscribe('position.markers', () => {
            this.clearAllMarkers();
            for (const marker of this.state.position.markers) {
                this.addMarker(marker);
            }
        });
        this.subscribe('globe.display', async () => {
            await this.onGlobeToggled();
            this.onCameraChanged(this.state.globe.camera);
        });
        this.subscribe('globe.shadows', (_oldShadows, newShadows) => this.onShadowsToggled(newShadows));
        this.subscribe('globe.shadowsTimestamp', (_oldTimestamp, newTimestamp) => this.onShadowsTimestampChanged(newTimestamp));
        this.subscribe('globe.camera', async (_oldCamera, newCamera) => {
            await this.onGlobeToggled();
            this.onCameraChanged(newCamera);
        });
        this.subscribe(/layers\.layersList\..*\.activeState/, async (_oldActive, _newActive, layer) => {
            await this.onLayerToggled(layer);
            if (layer.active) {
                this.onChangeSwiped(layer);
            }
        });
        this.subscribe(/layers\.layersList\..*\.opacity/, (_oldOpacity, _newOpacity, layer) => this.onChangeLayerOpacity(layer));
        this.subscribe(/layers\.layersList\..*\.swiped/, (_oldOpacity, _newOpacity, layer) => this.onChangeSwiped(layer));
        this.subscribe(/layers\.layersList\..*\.filter/, (_oldFilter, _newFilter, layer) => this.onChangeFilter(layer));
        this.subscribe(/layers\.layersList\..*\.timeRestriction/, (_oldTime, _newTime, layer) => this.onChangeTime(layer));
        this.subscribe(/layers\.layersList\..*\.order/, () => this.onChangeOrder());
        this.subscribe('application.isReady', (_, isLoaded) => {
            if (isLoaded) {
                // The map component may not be fully ready when other part of the application will be initialized
                // And some operations that need a fuly loaded map need to be first done when the application is ready
                // For example, Permalink needs resolution,
                this.onCameraChanged(this.state.globe.camera);
                // If the app is opened from a permalink, prioritize this data over settings in the shared state
                if (this.context.permalinkManager.hasFeatureSelectionQuery()) {
                    this.applyFeatureSelectionFromPermalink();
                }
                else if (this.context.permalinkManager.hasMapPosition()) {
                    this.applyMapPositionFromPermalink();
                }
                else {
                    this.applyFeatureSelectionFromSharedState();
                }
                this.showCrosshair(this.state.position);
                for (const marker of this.state.position.markers) {
                    this.addMarker(marker);
                }
            }
        });
    }
    locateUser() {
        if ('geolocation' in navigator) {
            // The user's current position is utilized to show the distance to objects in the layer
            navigator.permissions.query({ name: 'geolocation' }).then((result) => {
                if (result.state === 'granted' || result.state === 'prompt') {
                    this.shadow.getElementById('disable-location')?.classList.remove('hidden');
                    this.getCurrentLocation();
                }
            });
        }
        else {
            void window.gAlert('Geolocation browser error', 'Error');
        }
    }
    getCurrentLocation() {
        navigator.geolocation.getCurrentPosition(this.updateGeolocation, function (positionError) {
            switch (positionError.code) {
                case positionError.PERMISSION_DENIED:
                    void window.gAlert('Geolocation permission denied', 'Error');
                    break;
                case positionError.POSITION_UNAVAILABLE:
                    void window.gAlert('Geolocation permission unavailable', 'Error');
                    break;
                case positionError.TIMEOUT:
                    void window.gAlert('Geolocation timeout', 'Error');
                    break;
                default:
                    void window.gAlert('Geolocation error', 'Error');
                    break;
            }
        }, {
            enableHighAccuracy: true,
            timeout: 5000,
            maximumAge: 0
        });
    }
    disableLocateUser = () => {
        this.geolocationSource.clear();
        this.shadow.getElementById('disable-location')?.classList.add('hidden');
    };
    /**
     * Toggles the application fullscreen mode.
     * The button is displayed over the map, like the geolocation button.
     */
    toggleFullscreen() {
        this.state.interface.fullscreenMode = !this.state.interface.fullscreenMode;
    }
    /**
     * Shows the matching fullscreen button (enter or exit) depending on the current mode.
     */
    updateFullscreenButton(fullscreen) {
        this.shadow.getElementById('fullscreen-enter-button')?.classList.toggle('hidden', fullscreen);
        this.shadow.getElementById('fullscreen-exit-button')?.classList.toggle('hidden', !fullscreen);
    }
    updateGeolocation = (position) => {
        const coords = position.coords;
        const longitude = coords.longitude;
        const latitude = coords.latitude;
        const accuracy = coords.accuracy; //Accuracy radius in meters
        const code = this.olMap.getView().getProjection().getCode();
        const maxExtent = this.config.map.maxExtent?.split(',').map(Number);
        const numbers = parseCoordinates([longitude, latitude], maxExtent, code);
        this.geolocationSource.clear();
        // point of location
        const positionFeature = new Feature({
            geometry: new Point(numbers),
            type: 'position'
        });
        this.geolocationSource.addFeature(positionFeature);
        // radius accuracy
        const circleGeometry = new CircleGeom(numbers, accuracy);
        const accuracyFeature = new Feature({
            geometry: circleGeometry,
            type: 'accuracy'
        });
        this.geolocationSource.addFeature(accuracyFeature);
        this.olMap.getView().setCenter(numbers);
    };
    render() {
        super.render();
        // Read out the default projection from config, ignoring user preferences
        this.defaultSrid = this.context.configManager.getDefaultConfigValue('map.srid');
        // Initialize the map element
        this.mapTarget = this.shadow.getElementById('ol-map');
        this.map3dTarget = this.shadow.getElementById('cs-map');
        this.olMap.setTarget(this.mapTarget);
        // Initialize managers
        this.osmManager = new OsmManager(this.olMap);
        this.cogManager = new CogManager(this.olMap);
        this.xyzManager = new XyzManager(this.olMap);
        this.drawingManager = new DrawingManager(this.olMap);
        this.viewManager = new ViewManager(this.olMap, this.context.configManager, this.context.stateManager);
        this.vectorTilesManager = new VectorTilesManager(this.olMap);
        this.wmtsManager = new WmtsManager(this.olMap, this.context.stateManager);
        this.swiper = this.shadow.getElementById('swiper');
        this.closeSwiperButton = this.shadow.getElementById('close-swiper');
        this.swipeManager = new SwipeManager(this.olMap, this.swiper, this.closeSwiperButton, this.wmtsManager, this.context.wmsManager, this.context.localFileManager);
        // View
        const view = this.viewManager.getDefaultView();
        this.olMap.setView(view);
        // Create layer for highlighted features
        const highlightSource = new VectorSource({
            features: this.highlightedFeaturesCollection
        });
        // Create layer for selection
        const selectionSource = new VectorSource({
            features: this.selectedFeaturesCollection
        });
        this.selectionLayer = new VectorLayer({
            properties: {
                addToPrintedLayers: true
            },
            source: selectionSource,
            style: this.selectionLayerStyleFunc
        });
        this.highlightLayer = new VectorLayer({
            source: highlightSource
        });
        this.setSelectLayerStyle();
        this.setHighlightLayerStyle();
        this.olMap.addLayer(this.selectionLayer);
        this.olMap.addLayer(this.highlightLayer);
        this.olMap.addLayer(this.markerLayer);
        this.selectionLayer.setZIndex(1002);
        this.highlightLayer.setZIndex(1003);
        this.selectionLayer.set('altitudeMode', 'clampToGround');
        if (this.config.map.showScaleLine) {
            const scaleLine = new ScaleLine({
                units: 'metric'
            });
            this.olMap.addControl(scaleLine);
        }
        //layer user location
        this.geolocationSource = new VectorSource();
        const geolocationLayer = new VectorLayer({
            source: this.geolocationSource,
            style: new Style({
                image: new CircleStyle({
                    radius: 7,
                    fill: new Fill({
                        color: 'rgba(225,18,18,0.48)'
                    }),
                    stroke: new Stroke({
                        color: 'rgb(225,18,18)',
                        width: 2
                    })
                }),
                fill: new Fill({
                    color: 'rgba(20,100,213,0.49)'
                }),
                stroke: new Stroke({
                    color: 'rgb(20,100,213)',
                    width: 1
                })
            })
        });
        this.olMap.addLayer(geolocationLayer);
        // TODO REG: This is ugly, but I didn't find any other solution yet.
        setTimeout(() => {
            this.olMap.updateSize();
        }, 1000);
        this.mapTargetResizeObserver = new ResizeObserver(() => {
            this.updateCloseSwiperPosition();
        });
        this.mapTargetResizeObserver.observe(this.mapTarget);
    }
    listenOpenLayersEvents() {
        // https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html
        //this.olMap.on('change', (e) => console.log(e));
        //this.olMap.on('click', (e) => console.log(e));
        //this.olMap.on('dblclick', (e) => console.log(e));
        //this.olMap.on('error', (e) => console.log(e));
        this.olMap.on('loadstart', (e) => this.onLoadStart(e));
        this.olMap.on('loadend', (e) => this.onLoadEnd(e));
        //this.olMap.on('movestart', (e) => console.log(e));
        //this.olMap.on('pointerdrag', (e) => console.log(e));
        //this.olMap.on('postcompose', (e) => console.log(e));
        //this.olMap.on('postrender', (e) => console.log(e));
        //this.olMap.on('precompose', (e) => console.log(e));
        //this.olMap.on('propertychange', (e) => console.log(e));
        //this.olMap.on('rendercomplete', (e) => console.log(e));
        //? change:layerGroup
        //? change:size
        //? change:target
        //? change:view
        // Register all mouse and keyboard interactions with the UserInteractionManager as non-exclusive
        // events. If another tool registers one of these events exclusively, the listeners bellow will
        // be paused temporarily and reactivate once the tool is closed.
        // Select features via GetFeatureInfo
        if (this.registerInteractionListener('map.select', false)) {
            // Simple click for single feature selection
            this.olMap.on('singleclick', (e) => {
                if (this.mapMarkerUnderCursor) {
                    const mapMarker = this.mapMarkerUnderCursor.get('mapMarker');
                    if (mapMarker) {
                        this.state.position.tooltip = {
                            title: mapMarker.tooltip?.title,
                            content: mapMarker.tooltip?.content ?? '',
                            position: mapMarker.position
                        };
                        return;
                    }
                }
                if (this.canExecute('map.select')) {
                    this.onClick(e);
                }
            });
            // Dragbox interaction for multiple features selection
            this.dragbox = new DragBox({
                condition: (e) => platformModifierKeyOnly(e) && this.canExecute('map.select')
            });
            this.olMap.addInteraction(this.dragbox);
            this.dragbox.on('boxend', (e) => this.onDragSelection(e));
        }
        if (this.registerInteractionListener('map.mousemove', false)) {
            // Current map position
            this.olMap.on('moveend', (e) => {
                if (this.canExecute('map.mousemove')) {
                    this.onMoveEnd(e);
                }
            });
            // Cursor coordinates
            this.olMap.on('pointermove', (e) => {
                if (this.canExecute('map.mousemove')) {
                    this.onPointerMove(e);
                }
            });
        }
    }
    onLoadStart(_e) {
        this.state.loading = true;
    }
    onLoadEnd(_e) {
        this.state.loading = false;
    }
    onPointerMove(e) {
        this.state.mouseCoordinates = e.coordinate;
        const featuresAtPixel = this.olMap.getFeaturesAtPixel(e.pixel, {
            layerFilter: (layer) => layer === this.markerLayer
        });
        if (featuresAtPixel.length > 0) {
            this.mapMarkerUnderCursor = featuresAtPixel[0];
            const mapMarker = this.mapMarkerUnderCursor.get('mapMarker');
            if (mapMarker?.tooltip && this.mapTarget.style.cursor !== 'pointer') {
                this.previousMapCursorStyle = this.mapTarget.style.cursor;
                this.mapTarget.style.cursor = 'pointer';
            }
        }
        else {
            this.mapMarkerUnderCursor = null;
            this.mapTarget.style.cursor = this.previousMapCursorStyle;
        }
    }
    onMoveEnd(_e) {
        const view = this.olMap.getView();
        const newPosition = this.state.position.clone();
        newPosition.center = view.getCenter();
        newPosition.zoom = view.getZoom();
        newPosition.resolution = view.getResolution();
        newPosition.scale = this.viewManager.getScale();
        if (newPosition.isValid) {
            this.state.position = newPosition;
        }
    }
    onClick(e) {
        // Build the selection box using the default tolerance. Note that the origin of the pixel coordinates is in
        // the top left corner, whereas the origin of the map coordinates is in the bottom left corner.
        // To get a valid bounding box [xMin, yMin, xMax, yMax] is needed.
        const lowerLeftPixel = [e.pixel[0] - this.pixelTolerance, e.pixel[1] + this.pixelTolerance];
        const lowerLeftCoord = this.olMap.getCoordinateFromPixel(lowerLeftPixel);
        const topRightPixel = [e.pixel[0] + this.pixelTolerance, e.pixel[1] - this.pixelTolerance];
        const topRightCoord = this.olMap.getCoordinateFromPixel(topRightPixel);
        this.select([lowerLeftCoord[0], lowerLeftCoord[1], topRightCoord[0], topRightCoord[1]]);
    }
    onDragSelection(_e) {
        const extent = this.dragbox.getGeometry().getExtent();
        this.select(extent);
    }
    select(extent) {
        // Reset current selection if SelectionMode is Replace
        if (this.state.selection.selectionMode === SelectionMode.Replace) {
            this.selectNone();
        }
        else if (this.state.selection.selectionMode === SelectionMode.Add) {
            this.state.selection.selectionParameters = [];
        }
        // Layers selectable today are WMS, WMTS (with wms layer) and Local files
        // Use batch for changes to prevent multiple selection of objects
        this.context.stateManager.batchChanges(() => {
            this.context.wmsManager.selectFeatures(extent);
            this.wmtsManager.selectFeatures(extent);
            this.context.localFileManager.selectFeatures(extent);
        });
    }
    selectNone() {
        this.state.selection.selectedFeatures = [];
        this.state.selection.selectionParameters = [];
        this.state.selection.highlightedFeatures = [];
    }
    async onSelectFeatures(selectionParams) {
        if (selectionParams.length === 0) {
            return;
        }
        this.state.loading = true;
        try {
            // WMS GetFeatureInfo
            const wmsPromises = selectionParams.map((param) => {
                const wmsGetFeatureInfoSelectionParam = param.clone((l) => l.queryable && l.wmsQueryableOnly);
                const client = this.context.wmsManager.getClient(wmsGetFeatureInfoSelectionParam.ogcServer);
                return client.getFeatureInfo(wmsGetFeatureInfoSelectionParam);
            });
            // WFS GetFeature
            const wfsPromises = selectionParams.map((param) => {
                const wfsGetFeatureInfoSelectionParam = param.clone((l) => l.wfsQueryable);
                const client = this.context.wfsManager.getClient(wfsGetFeatureInfoSelectionParam.ogcServer);
                const features = client.getFeature(wfsGetFeatureInfoSelectionParam);
                return features;
            });
            const wmsGmlFeatures = (await Promise.all(wmsPromises)).flat();
            const wfsGmlFeatures = (await Promise.all(wfsPromises)).flat();
            const gmlFeatures = [...wmsGmlFeatures, ...wfsGmlFeatures];
            if (gmlFeatures.length === 0 && this.state.selection.selectedFeatures.length === 0) {
                this.state.interface.selectionComponentVisible = false;
            }
            else {
                applyFeaturesToSelection(gmlFeatures, this.state);
            }
        }
        finally {
            this.state.loading = false;
        }
    }
    connectedCallback() {
        super.connectedCallback();
        this.olMap = this.context.mapManager.getMap();
        this.focusFeature = new FocusFeature(this.olMap, this.context.configManager);
        this.render();
        this.listenOpenLayersEvents();
        this.registerEvents();
        this.onGlobeToggled();
    }
    onCustomGirafeEvent(details) {
        if (details.action === GeoEvents.zoomToExtent) {
            this.zoomToExtent(details.extent);
        }
    }
    onChangeSwiped(layer) {
        this.swipeManager.toggleSwipe(layer);
        this.updateCloseSwiperPosition();
    }
    /**
     * updates icon position when swipe is moved
     */
    updateCloseSwiperPosition() {
        const sliderValue = Number.parseFloat(this.swiper.value);
        const max = Number.parseFloat(this.swiper.max);
        const min = Number.parseFloat(this.swiper.min);
        const percent = (sliderValue - min) / (max - min);
        const offset = percent * this.swiper.offsetWidth;
        this.closeSwiperButton.style.left = `${offset}px`;
    }
    setSelectLayerStyle() {
        this.selectionLayerStyle = new Style({
            stroke: new Stroke({
                color: this.config.selection.defaultStrokeColor,
                width: this.config.selection.defaultStrokeWidth
            }),
            fill: new Fill({ color: this.config.selection.defaultFillColor }),
            image: new Circle({
                radius: 7,
                fill: new Fill({ color: this.config.selection.defaultFillColor }),
                stroke: new Stroke({
                    color: this.config.selection.defaultStrokeColor,
                    width: this.config.selection.defaultStrokeWidth
                })
            })
        });
    }
    setHighlightLayerStyle() {
        this.highlightLayer.setStyle(new Style({
            stroke: new Stroke({
                color: this.config.selection.highlightStrokeColor,
                width: this.config.selection.defaultStrokeWidth
            }),
            fill: new Fill({ color: this.config.selection.highlightFillColor }),
            image: new Circle({
                radius: 7,
                fill: new Fill({ color: this.config.selection.highlightFillColor }),
                stroke: new Stroke({
                    color: this.config.selection.highlightStrokeColor,
                    width: this.config.selection.defaultStrokeWidth
                })
            })
        }));
    }
    setCrosshairStyle() {
        this.crosshairLayer.setStyle(new Style({
            image: new RegularShape({
                fill: new Fill({ color: this.config.selection.defaultFillColor }),
                stroke: new Stroke({ color: this.config.selection.defaultStrokeColor, width: 2 }),
                points: 4,
                radius: 10,
                radius2: 0,
                angle: 0
            })
        }));
    }
    async create3dMap() {
        if (!this.map3d && this.config.map3d) {
            this.loading = true;
            super.render();
            // First : Lazy loading of Cesium and olcs
            const Cesium = await import('cesium');
            window.Cesium = Cesium;
            const olcs = await import('olcs');
            // Initialize the 3D Map
            this.map3d = new olcs.default({
                map: this.olMap,
                target: this.map3dTarget,
                time: () => {
                    const date = new Date(this.map3dShadowsTimestamp);
                    return Number.isNaN(date.getTime()) ? Cesium.JulianDate.now() : Cesium.JulianDate.fromDate(date);
                }
            });
            const scene = this.map3d.getCesiumScene();
            const config = this.config.map3d;
            scene.screenSpaceCameraController.maximumZoomDistance = config.maximumZoomDistance ?? 30000;
            // Add terrain
            if (config.terrainUrl) {
                scene.terrainProvider = await Cesium.CesiumTerrainProvider.fromUrl(config.terrainUrl);
            }
            // Add terrain imagery
            let coverage = Cesium.Rectangle.MAX_VALUE;
            if (config.terrainImagery) {
                if (config.terrainImagery.coverageArea) {
                    coverage = Cesium.Rectangle.fromDegrees(...config.terrainImagery.coverageArea);
                }
                scene.imageryLayers.addImageryProvider(new Cesium.UrlTemplateImageryProvider({
                    url: config.terrainImagery.url,
                    minimumLevel: config.terrainImagery.minLoD ?? 0,
                    maximumLevel: config.terrainImagery.maxLoD,
                    tilingScheme: config.terrainImagery.srid === 3857
                        ? new Cesium.WebMercatorTilingScheme()
                        : new Cesium.GeographicTilingScheme(),
                    rectangle: coverage
                }));
            }
            // Add 3D-Tiles layers
            const tilesetOptions = {
                // If the error of the model is higher than this, we increase the resolution
                maximumScreenSpaceError: 0.5,
                // Enable different level of details based on the distance from the camera
                dynamicScreenSpaceError: true,
                // Model error at the max distance from the camera (higher = distant models are of lower quality)
                dynamicScreenSpaceErrorFactor: config.tilesetsMaxError ?? 7
            };
            config.tilesetsUrls.forEach((tilesetUrl) => {
                Cesium3DTileset.fromUrl(tilesetUrl, tilesetOptions).then((t) => scene.primitives.add(t));
            });
            // REG: ambientOcclusion was temporary deactivated because of performance impact and horizontal lines artefacts
            // const ambientOcclusion = scene.postProcessStages.ambientOcclusion;
            // ambientOcclusion.enabled = true;
            // ambientOcclusion.uniforms.bias = 0.5;
            // ambientOcclusion.uniforms.stepSize = 1;
            // ambientOcclusion.uniforms.blurStepSize = 1;
            // REG: Adding the following line solves the problem, but it remains a log less performatn with ambiant occlusion.
            // So I don't know what we want to do with it.
            // See https://github.com/CesiumGS/cesium/issues/13039#issuecomment-3583233494
            // viewer.camera.frustum.near = 1.0;
            this.loading = false;
            this.state.globe.loaded = true;
            super.render();
            const cesiumScreenToLocalCoord = (position) => {
                const cart = Cesium.Cartographic.fromCartesian(pickOnGlobe(position));
                const longLat = [Cesium.Math.toDegrees(cart.longitude), Cesium.Math.toDegrees(cart.latitude)];
                return proj4('EPSG:4326', this.config.map.srid, longLat);
            };
            const pickOnGlobe = (position) => {
                const ray = scene.camera.getPickRay(position);
                return ray == undefined ? undefined : scene.globe.pick(ray, scene);
            };
            this.registerInteractionListener('globe.select', true);
            const eventHandler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
            eventHandler.setInputAction((event) => {
                // If the click is on the map and selection is allowed
                if (Cesium.defined(event.position) && this.canExecute('globe.select')) {
                    const topLeftScreen = event.position.clone();
                    topLeftScreen.x -= this.pixelTolerance;
                    topLeftScreen.y -= this.pixelTolerance;
                    const bottomRightScreen = event.position.clone();
                    bottomRightScreen.x += this.pixelTolerance;
                    bottomRightScreen.y += this.pixelTolerance;
                    const topLeft = cesiumScreenToLocalCoord(topLeftScreen);
                    const bottomRight = cesiumScreenToLocalCoord(bottomRightScreen);
                    this.select([topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]]);
                }
            }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
            this.wmsManager3d = new WmsManager3d(scene, this.context);
            this.state.layers.layersList.forEach((l) => this.addAllActiveLayers3dMap(l));
            const camera = scene.camera;
            camera.changed.addEventListener(() => {
                console.debug('Cesium camera moved');
                this.state.globe.camera = {
                    heading: camera.heading,
                    pitch: camera.pitch,
                    roll: camera.roll
                };
            }, 1);
        }
    }
    addAllActiveLayers3dMap(layer) {
        if (layer instanceof LayerWms) {
            if (layer.active) {
                this.wmsManager3d?.addLayer(layer);
            }
        }
        else if (layer instanceof GroupLayer) {
            layer.children.forEach((l) => this.addAllActiveLayers3dMap(l));
        }
    }
    async onGlobeToggled() {
        if (this.state.globe.display === '3D') {
            // Full screen globe has been enabled
            this.mapTarget.style.display = 'none';
            this.map3dTarget.style.display = 'block';
            this.map3dTarget.style.left = '0';
            this.map3dTarget.style.width = '100%';
            await this.create3dMap();
            this.map3d.setEnabled(true);
        }
        else if (this.state.globe.display === '2D/3D') {
            // Side by side has been enabled
            this.mapTarget.style.display = 'inline-block';
            this.mapTarget.style.width = '45%';
            this.map3dTarget.style.display = 'inline-block';
            this.map3dTarget.style.left = '45%';
            this.map3dTarget.style.width = '55%';
            await this.create3dMap();
            this.map3d.setEnabled(true);
        }
        else {
            // 3d map is not visible
            if (this.map3d) {
                this.map3d.setEnabled(false);
            }
            this.mapTarget.style.display = 'block';
            this.mapTarget.style.width = '100%';
            this.map3dTarget.style.display = 'none';
            this.unregisterInteractionListeners('globe.select');
        }
    }
    onShadowsToggled(shadows) {
        if (this.map3d) {
            const scene = this.map3d.getCesiumScene();
            scene.shadowMap.enabled = scene.globe.enableLighting = shadows;
        }
    }
    onShadowsTimestampChanged(shadowsTimestamp) {
        if (this.map3d) {
            this.map3dShadowsTimestamp = shadowsTimestamp;
        }
    }
    onFeaturesSelected(features) {
        this.selectedFeaturesCollection.clear();
        if (features) {
            for (const feature of features) {
                this.selectedFeaturesCollection.push(feature);
            }
            // Recreate the style in case user preferences have changed in the meantime
            this.setSelectLayerStyle();
        }
    }
    onFeatureHighlighted(features) {
        this.highlightedFeaturesCollection.clear();
        for (const feature of features) {
            this.highlightedFeaturesCollection.push(feature);
        }
        // Recreate the style in case user preferences have changed in the meantime
        this.setHighlightLayerStyle();
    }
    onCameraChanged(camera) {
        if (this.map3d && camera) {
            const scene = this.map3d.getCesiumScene();
            scene.camera.setView({
                destination: scene.camera.position, // Keep current position
                orientation: {
                    heading: camera.heading,
                    pitch: camera.pitch,
                    roll: camera.roll
                }
            });
        }
    }
    zoomToExtent(extent) {
        this.olMap.getView().fit(extent);
    }
    onChangeProjection(_oldSrid, newSrid) {
        const newView = this.viewManager.getViewConvertedToSrid(newSrid);
        this.olMap.setView(newView);
    }
    onChangeDarkMode() {
        if (this.state.interface.darkMapMode) {
            this.mapTarget.classList.add('darkmap');
        }
        else {
            this.mapTarget.classList.remove('darkmap');
        }
    }
    async onLayerToggled(layer) {
        if (layer instanceof Layer) {
            if (layer.active) {
                await this.onAddLayers([layer]);
            }
            else {
                this.onRemoveLayers([layer]);
            }
        }
    }
    async onAddLayers(layerInfos) {
        for (const l of layerInfos) {
            if (l instanceof LayerWms) {
                this.context.wmsManager.getClient(l).addLayer(l);
                if (this.wmsManager3d != null)
                    this.wmsManager3d.addLayer(l);
            }
            else if (l instanceof LayerWmts) {
                await this.wmtsManager.addLayer(l);
            }
            else if (l instanceof LayerLocalFile) {
                this.context.localFileManager.addLayer(l);
            }
            else if (l instanceof LayerCog) {
                this.cogManager.addLayer(l);
            }
            else if (l instanceof LayerXYZ) {
                this.xyzManager.addLayer(l);
            }
            else if (l instanceof LayerDrawing) {
                this.drawingManager.addLayer(l);
            }
        }
    }
    onRemoveLayers(layerInfos) {
        for (const l of layerInfos) {
            if (l instanceof LayerWms) {
                this.context.wmsManager.getClient(l).removeLayer(l);
                if (this.wmsManager3d != null) {
                    this.wmsManager3d.removeLayer(l);
                }
            }
            else if (l instanceof LayerWmts) {
                this.wmtsManager.removeLayer(l);
            }
            else if (l instanceof LayerLocalFile) {
                this.context.localFileManager.removeLayer(l);
            }
            else if (l instanceof LayerCog) {
                this.cogManager.removeLayer(l);
            }
            else if (l instanceof LayerXYZ) {
                this.xyzManager.removeLayer(l);
            }
            else if (l instanceof LayerDrawing) {
                this.drawingManager.removeLayer(l);
            }
        }
    }
    onChangeOrder = debounce(() => this.reorderLayers(), 0);
    reorderLayers() {
        this.wmtsManager.refreshZIndexes();
        this.context.wmsManager.refreshZIndexes();
    }
    onChangeLayerOpacity(layerInfos) {
        if (layerInfos instanceof LayerWms) {
            this.context.wmsManager.getClient(layerInfos).changeOpacity(layerInfos);
            if (this.wmsManager3d != null)
                this.wmsManager3d.changeOpacity(layerInfos);
        }
        else if (layerInfos instanceof LayerWmts) {
            if (this.wmtsManager.layerExists(layerInfos)) {
                this.wmtsManager.changeOpacity(layerInfos);
            }
        }
        else if (layerInfos instanceof LayerLocalFile) {
            this.context.localFileManager.changeOpacity(layerInfos);
        }
        else if (layerInfos instanceof LayerDrawing) {
            this.drawingManager.changeOpacity(layerInfos);
        }
        else if (layerInfos instanceof LayerOsm) {
            this.osmManager.changeOpacity(layerInfos);
        }
        else {
            console.warn(`Changing opacity for layer ${layerInfos.name} of type ${typeof layerInfos} not supported`);
        }
    }
    onChangeBasemapOpacity(basemap) {
        applyOpacityToLayers(basemap.opacity, basemap.layersList, (layer) => this.onChangeLayerOpacity(layer));
    }
    /**
     * Change filter configuration on layer (only LayerWMS are affected)
     */
    onChangeFilter(layerInfos) {
        this.context.wmsManager.getClient(layerInfos).changeFilter(layerInfos);
        if (this.wmsManager3d != null)
            this.wmsManager3d.changeFilter(layerInfos);
    }
    /**
     * Change time configuration on layer (only LayerWMS are affected)
     */
    onChangeTime(layer) {
        if (layer instanceof LayerWms && layer.active) {
            this.context.wmsManager.getClient(layer).changeTimeRestriction(layer);
        }
        else if (layer instanceof GroupLayer) {
            this.context.stateManager.batchChanges(() => {
                // Recursively apply the time restriction to all children of the group layer
                this.setTimeRestrictionOnChildren(layer, layer.timeRestriction);
            });
        }
    }
    setTimeRestrictionOnChildren(layer, newTime) {
        for (const childLayer of layer.children) {
            if (childLayer instanceof GroupLayer) {
                this.setTimeRestrictionOnChildren(childLayer, newTime);
            }
            else if (isTimeAwareLayer(childLayer) && childLayer.timeRestriction !== newTime) {
                childLayer.timeRestriction = newTime;
            }
        }
    }
    removeAllBasemapLayers() {
        this.wmtsManager.removeAllBasemapLayers();
        this.context.wmsManager.removeAllBasemapLayers();
        if (this.wmsManager3d != null)
            this.wmsManager3d.removeAllBasemapLayers();
        this.osmManager.removeAllBasemapLayers();
        this.cogManager.removeAllBasemapLayers();
        this.xyzManager.removeAllBasemapLayers();
        this.vectorTilesManager.removeAllBasemapLayers();
    }
    onChangeBasemaps(basemaps) {
        this.removeAllBasemapLayers();
        for (const layer of basemaps.flatMap((basemap) => basemap.layersList)) {
            if (layer instanceof LayerOsm) {
                this.osmManager.addBasemapLayer(layer);
            }
            else if (layer instanceof LayerVectorTiles) {
                this.vectorTilesManager.addBasemapLayer(layer);
            }
            else if (layer instanceof LayerCog) {
                this.cogManager.addBasemapLayer(layer);
            }
            else if (layer instanceof LayerXYZ) {
                this.xyzManager.addBasemapLayer(layer);
            }
            else if (layer instanceof LayerWmts) {
                this.wmtsManager.addBasemapLayer(layer);
            }
            else if (layer instanceof LayerWms) {
                this.context.wmsManager.getClient(layer).addBasemapLayer(layer);
                if (this.wmsManager3d != null)
                    this.wmsManager3d.addBasemapLayer(layer);
            }
            else {
                throw new TypeError('Unknown basemap type');
            }
        }
        // Apply default opacity
        for (const basemap of basemaps) {
            if (!basemap.opacityDisabled) {
                this.onChangeBasemapOpacity(basemap);
            }
        }
    }
    /**
     * This method checks for the presence of an initial selection originating from a shared state.
     * If present, it applies the query-based or bbox-based selection to the map.
     */
    applyFeatureSelectionFromSharedState() {
        if (this.state.selection.initialSelectionQuery) {
            const layerName = this.state.selection.initialSelectionQuery.layerName;
            const query = this.state.selection.initialSelectionQuery.query;
            const layer = this.context.layerManager.getTreeItemByLayerName(layerName);
            if (layer && layer instanceof LayerWms) {
                this.selectFeaturesByQuery(layer, query, false);
            }
        }
        else if (this.state.selection.initialSelectionBox) {
            this.select(this.state.selection.initialSelectionBox);
        }
    }
    /**
     * Applies a feature selection based on the query parameters present in the URL starting with `wfs_`.
     * If the specified layer does not exist in the current tree, it's fetched from the themes and added to the tree.
     * If the layer is resolution-restricted, the map is zoomed to a resolution where the layer is visible.
     */
    applyFeatureSelectionFromPermalink() {
        const featureSelectionFromUrl = this.context.permalinkManager.getFeatureSelectionQuery();
        if (!featureSelectionFromUrl) {
            return;
        }
        let layerInTree = this.context.layerManager.getTreeItemByLayerName(featureSelectionFromUrl.layer);
        if (!layerInTree) {
            const layer = this.context.themesHelper.findLayerByName(featureSelectionFromUrl.layer);
            if (!layer) {
                throw new Error(`Layer ${featureSelectionFromUrl.layer} cannot be found`);
            }
            const clonedTheme = this.context.themesHelper.getMinimalClonedThemeForLayer(layer);
            clonedTheme.isExpanded = true;
            this.context.themesHelper.mergeThemeInLayerTree(clonedTheme, true, true);
            // Now, get the tree item
            layerInTree = this.context.layerManager.getTreeItemByLayerName(featureSelectionFromUrl.layer);
        }
        if (!(layerInTree && layerInTree instanceof LayerWms && layerInTree.wfsQueryable)) {
            throw new Error(`Can't apply feature selection from permalink, layer ${featureSelectionFromUrl.layer} does not exist or does not support querying`);
        }
        // Make sure the layer is rendered in the map, otherwise WFS querying won't work
        const currentResolution = this.olMap.getView().getResolution();
        if (layerInTree.minResolution && currentResolution && layerInTree.minResolution > currentResolution) {
            this.state.position.resolution = layerInTree.minResolution;
        }
        else if (layerInTree.maxResolution && currentResolution && layerInTree.maxResolution < currentResolution) {
            this.state.position.resolution = layerInTree.maxResolution;
        }
        if (layerInTree.activeState !== 'on') {
            this.context.layerManager.toggleLayer(layerInTree, 'on');
        }
        // Create a wfs filter expression combining all filter conditions with an 'AND' operator
        const query = new WfsFilter(featureSelectionFromUrl.properties.map((property) => 
        // Note: PropertyType 'string' will result in correct WFS filters for both strings and numbers
        new WfsFilterCondition(property.name, 'eq', property.value, '', 'string')), 'and');
        this.selectFeaturesByQuery(layerInTree, query);
    }
    /**
     * Selects features based on the given list of wfs filters for a specified WMS layer
     * and optionally moves the map to the selection. The WFS client combines the WFS filters with an AND operator.
     */
    selectFeaturesByQuery(layerInTree, query, moveMapToSelection = true) {
        // Listen for the completed features selection, then move the map to the selected features
        if (moveMapToSelection) {
            const selectSubscription = this.subscribe('selection.selectedFeatures', (_, newFeatures) => {
                this.unsubscribe(selectSubscription);
                this.centerMapOnFeatures(newFeatures);
            });
        }
        // Trigger the selection
        this.context.wmsManager.selectFeaturesByQuery(query, layerInTree);
    }
    /**
     * Centers the map view on the given features. If the features' extent doesn't fit in the current zoom level,
     * the zoom level is adjusted.
     */
    centerMapOnFeatures(features) {
        const selectedGeometries = new GeometryCollection(features.map((f) => f.getGeometry()).filter((f) => f != undefined));
        const featuresExtent = selectedGeometries.getExtent();
        const mapExtent = this.olMap.getView().calculateExtent();
        if (getWidth(featuresExtent) <= getWidth(mapExtent) && getHeight(featuresExtent) <= getHeight(mapExtent)) {
            // Move the map so all selected features are visible -> this won't change the zoom level
            this.state.position.center = getCenter(featuresExtent);
        }
        else {
            // Fit the map view so all features are visible -> this will change the zoom level
            this.zoomToExtent(featuresExtent);
        }
    }
    /**
     * Moves the map to the position defined in the permalink, making sure the map is initialized and ready to be moved.
     */
    applyMapPositionFromPermalink() {
        const position = this.context.permalinkManager.getMapPosition(this.projection);
        if (position?.isValid) {
            // We need the following to recalculate resolution and scale to properly update the position state
            this.state.position = position;
            if (position.markers.length > 0) {
                // Add marker to the map
                this.addMarker(position.markers[0]);
            }
        }
    }
    clearAllMarkers() {
        this.markerSource.clear();
    }
    addMarker(mapMarker) {
        /*
         In OpenLayers we need to calculate the anchor based on the marker size and offset as it is `fraction` by default.
         As the offset and the size are provided in pixel, we need to calculate the fraction for each axis. So, for example,
         when you have size [21,25] and offset [-10.5,-25] the anchor must be [0.5, 1] to have the bottom center of the
         marker at the given coordinates.
         */
        const anchor = mapMarker.offset && mapMarker.size
            ? [Math.abs(mapMarker.offset[0] / mapMarker.size[0]), Math.abs(mapMarker.offset[1] / mapMarker.size[1])]
            : undefined;
        const iconStyle = new Style({
            image: new Icon({
                anchor: anchor,
                src: mapMarker.imageUrl,
                width: mapMarker.size ? mapMarker.size[0] : undefined,
                height: mapMarker.size ? mapMarker.size[1] : undefined
            })
        });
        const marker = new Feature({
            geometry: new Point(mapMarker.position)
        });
        marker.setStyle(iconStyle);
        marker.set('mapMarker', mapMarker);
        this.markerSource.addFeature(marker);
    }
    showCrosshair(position) {
        if (!position.crosshair) {
            return;
        }
        // Remove existing crosshair first
        if (this.crosshairFeature) {
            this.olMap.removeLayer(this.crosshairLayer);
        }
        // Set new crosshair
        this.crosshairFeature = new Feature(new Point(position.crosshair));
        this.crosshairLayer = new VectorLayer({ source: new VectorSource({ features: [this.crosshairFeature] }) });
        this.setCrosshairStyle();
        this.olMap.addLayer(this.crosshairLayer);
    }
    onFeaturesTypeChanged(_newFeaturesType) {
        console.log('Features type changed: ' + _newFeaturesType);
        this.selectionLayer.changed();
    }
}