@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
993 lines (991 loc) • 50.9 kB
JavaScript
import { html as uHtml } from 'uhtml';
import { Feature, Collection } from 'ol';
import { Style, Stroke, Fill, Circle, RegularShape } from 'ol/style';
import { get as getProjection, transform } from 'ol/proj';
import VectorSource from 'ol/source/Vector';
import VectorLayer from 'ol/layer/Vector';
import { platformModifierKeyOnly } from 'ol/events/condition';
import { DragBox } from 'ol/interaction';
import { ScaleLine } from 'ol/control';
import { Point } from 'ol/geom';
import { Cesium3DTileset } from 'cesium';
import proj4 from 'proj4';
import SwipeManager from './tools/swipemanager';
import WmsManager from '../../tools/wms/wmsmanager';
import WmsManager3d from './tools/wmsmanager3d';
import OsmManager from './tools/osmmanager';
import VectorTilesManager from './tools/vectortilesmanager';
import WmtsManager from './tools/wmtsmanager';
import ViewManager from './tools/viewmanager';
import LocalFileManager from './tools/localfilemanager';
import CogManager from './tools/cogmanager';
import GirafeHTMLElement from '../../base/GirafeHTMLElement';
import Layer from '../../models/layers/layer';
import LayerCog from '../../models/layers/layercog';
import LayerXYZ from '../../models/layers/layerxyz';
import LayerOsm from '../../models/layers/layerosm';
import LayerVectorTiles from '../../models/layers/layervectortiles';
import LayerWmts from '../../models/layers/layerwmts';
import LayerWms from '../../models/layers/layerwms';
import LayerLocalFile from '../../models/layers/layerlocalfile';
import GeoEvents from '../../models/events';
import MapManager from '../../tools/state/mapManager';
import MapPosition, { parseMapPositionFromUrl, setUrlFromMapPosition } from '../../tools/state/mapposition';
import GroupLayer from '../../models/layers/grouplayer';
import { FocusFeature } from './tools/focusfeature';
import XyzManager from './tools/xyzmanager';
import ThemeLayer from '../../models/layers/themelayer';
import { isTimeAwareLayer } from '../../models/layers/timeawarelayer';
import { debounce } from '../../tools/utils/debounce';
import WfsManager from '../../tools/wfs/wfsmanager';
import { isProjectionInDegrees, isCoordinateInDegrees } from '../../tools/utils/olutils';
export default class MapComponent extends GirafeHTMLElement {
get projection() {
return this.olMap.getView().getProjection();
}
constructor() {
super('map');
Object.defineProperty(this, "template", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
return uHtml `<style>
*{font-family:Arial,sans-serif}.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{color:var(--link-color)}a:visited{color:var(--link-color)}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}100%{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-iteration-count:infinite;animation-timing-function:linear}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-iteration-count:infinite;animation-timing-function:linear}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}.gg-button,.gg-input,.gg-select,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);outline:0;border-radius:3px;border:var(--app-standard-border);display:inline-block;box-sizing:border-box;padding:0 0 0 .5rem;margin:0;cursor:pointer}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;display:inline-block;padding:0;margin:0}.gg-button,.gg-input,.gg-label,.gg-select{min-height:calc(var(--app-standard-height)/ 1.5);max-height:calc(var(--app-standard-height)/ 1.5);line-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{padding:.5rem;height:6rem}.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-grad2);background-color:var(--text-color-grad2);color:var(--bkg-color)}.gg-button:disabled{background-color:#d3d3d3;color:grey;border:none;cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{border:none;background-color:transparent;display:flex;flex-direction:column;color:var(--text-color);padding:0;align-items:center;justify-content:center;cursor:pointer}.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{margin:0;width:calc(var(--app-standard-height) - 1.5rem)}.gg-big-withtext span{font-size:.9rem;font-variant:small-caps;padding:0 1rem}.gg-medium,.gg-medium-withtext{flex-direction:row;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)}.gg-medium img{margin:0;width:calc(var(--app-standard-height)/ 2.4)}.gg-medium-withtext img{margin-left:.5rem;width:calc(var(--app-standard-height)/ 2.4)}.gg-medium-withtext span{font-size:.9rem;padding:0 1rem;padding-left:.5rem}.gg-small,.gg-small-withtext{flex-direction:row;min-width:calc(var(--app-standard-height)/ 2);min-height:calc(var(--app-standard-height)/ 2);max-height:calc(var(--app-standard-height)/ 2)}.gg-small img{margin:0;width:calc(var(--app-standard-height)/ 3)}.gg-small-withtext img{margin-left:.5rem;width:calc(var(--app-standard-height)/ 3)}.gg-small-withtext span{font-size:.9rem;padding:0 .5rem;padding-left:.3rem}.gg-button:hover,.gg-icon-button:hover,.gg-input:hover,.gg-select:hover,.gg-textarea:hover{background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{background-color:transparent;opacity:1}.gg-tabs{display:flex;justify-content:left;margin:.5rem auto 0;border-bottom:1px solid grey;cursor:pointer;height:2rem;align-items:end;gap:1rem;margin-bottom:1rem}.gg-tab{border:none;background:0 0;cursor:pointer;padding:.5rem;color:var(--text-color)}.gg-tab.active{border-bottom:solid 2px var(--text-color-grad2);font-weight:600}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{border:none;background-color:transparent;display:flex;flex-direction:column;color:var(--text-color)}.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(1)}.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{margin:.3rem;height:2rem}.girafe-button-large span{line-height:2rem;height:2rem;margin:.3rem}.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{width:100%;text-align:left;overflow:hidden;text-overflow:ellipsis}.girafe-button-tiny{width:1rem;height:1rem;align-items:center;padding:0}.girafe-button-tiny img{overflow:hidden}
</style><style>
#container,#ol-map{background-color:var(--bkg-color);height:100%;width:100%}#ol-map.darkmap canvas{filter:invert(100%) hue-rotate(180deg)}.hidden{display:none}.center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);margin:10px;text-align:center}.loading span{color:var(--text-color);display:block;margin-top:1rem;font-weight:600}.loading span.quote{font-style:italic;font-weight:300}#cs-map{background-color:var(--bkg-color);position:absolute;height:100%;overflow:hidden;display:none;border-left:solid 3px #444}.ol-zoom{right:2rem!important;top:2rem!important;left:unset!important}.ol-rotate{top:7rem!important;right:2rem!important}.ol-rotate-reset,.ol-zoom-in,.ol-zoom-out{width:2rem!important;height:2rem!important;cursor:pointer}.ol-scale-line{right:1rem!important;bottom:.5rem!important;left:unset!important}.ol-popup{position:absolute;background-color:var(--bkg-color);color:var(--text-color);box-shadow:0 1px 4px var(--bx-shdw);padding:15px;border-radius:10px;border:1px solid var(--text-color-grad1);bottom:12px;left:-50px;min-width:140px}.ol-popup:after,.ol-popup:before{top:100%;border:solid transparent;content:' ';height:0;width:0;position:absolute;pointer-events:none}.ol-popup:after{border-top-color:var(--bkg-color);border-width:10px;left:48px;margin-left:-10px}.ol-popup:before{border-top-color:var(--text-color-grad1);border-width:11px;left:48px;margin-left:-11px}.ol-popup-closer{text-decoration:none;position:absolute;top:2px;right:8px;background-color:var(--bkg-color);color:var(--text-color);cursor:pointer}.ol-popup-closer:after{content:'✖'}#swiper{display:none;width:99%;position:absolute;left:.5%;top:50%;height:0}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;border:2px solid #444;height:0;margin-top:-1000px;padding-top:1000px;padding-bottom:1000px;width:6px;background:var(--bkg-color);cursor:ew-resize}input[type=range]::-moz-range-thumb{border:2px solid #444;height:0;padding-top:1000px;padding-bottom:1000px;width:3px;background:var(--bkg-color);cursor:ew-resize}.close-swiper{display:none;position:absolute;top:0;transform:translateX(-3px);width:2rem;height:2rem;background-color:var(--ol-background-color);color:var(--ol-subtle-foreground-color);border-radius:4px;border-width:1px;cursor:pointer}.close-swiper:focus,.close-swiper:hover{outline:1px solid var(--ol-subtle-foreground-color);color:var(--ol-foreground-color)}.ol-viewport .tooltip{position:relative;background:rgba(0,0,0,.5);border-radius:0;color:#fff;padding:.32rem .62rem;opacity:.7;white-space:nowrap}@media screen and (max-width:1000px){.ol-zoom{display:none!important}.ol-rotate{top:.5rem!important;right:.5rem!important}}.contextmenu{background-color:var(--bkg-color);color:var(--text-color);box-shadow:1px 3px 4px rgba(0,0,0,.4);border:1px solid #ccc}.menu-entry{cursor:pointer;padding:10px}
</style>
<link rel="stylesheet" href="lib/ol/ol.css" />
<div id="container">
<div id="ol-map"></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>
<div id="popup" class="ol-popup hidden">
<button
id="popup-closer"
class="ol-popup-closer gg-icon-button gg-small"
onclick="${() => this.closePopup()}"></button>
<div id="popup-content"></div>
</div>
<input id="swiper" type="range" min="0" max="1000" step="1" />
<button tip="Hide Swiper" tip-placement="bottom" id="close-swiper" class="close-swiper">×</button>
<div id="map-contextmenu"></div>
</div>
`;
}
});
Object.defineProperty(this, "olMap", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "mapTarget", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// 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;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.defineProperty(this, "map3d", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "map3dTarget", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "map3dShadowsTimestamp", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "loading", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "swiper", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "closeSwiperButton", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "swipeManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "wmtsManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "wmsManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "wmsManager3d", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "osmManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "cogManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "xyzManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "viewManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "vectorTilesManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "localFileManager", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "defaultSrid", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "crosshairFeature", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "crosshairLayer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "tooltipContainer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "tooltipOverlay", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// For object selection
Object.defineProperty(this, "selectedFeaturesCollection", {
enumerable: true,
configurable: true,
writable: true,
value: new Collection()
});
Object.defineProperty(this, "highlightedFeaturesCollection", {
enumerable: true,
configurable: true,
writable: true,
value: new Collection()
});
Object.defineProperty(this, "selectionLayer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "highlightLayer", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "pixelTolerance", {
enumerable: true,
configurable: true,
writable: true,
value: 10
});
Object.defineProperty(this, "dragbox", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "focusFeature", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// Remember initial position configuration from URL
Object.defineProperty(this, "initialPositionFromUrl", {
enumerable: true,
configurable: true,
writable: true,
value: parseMapPositionFromUrl()
});
Object.defineProperty(this, "onChangeOrder", {
enumerable: true,
configurable: true,
writable: true,
value: debounce(() => this.reorderLayers(), 0)
});
this.olMap = MapManager.getInstance().getMap();
this.focusFeature = new FocusFeature();
}
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('activeBasemap', (_oldBasemap, newBasemap) => this.onChangeBasemap(newBasemap));
this.subscribe('projection', (oldProjection, newProjection) => this.onChangeProjection(oldProjection, newProjection));
this.subscribe('interface.darkMapMode', (_oldValue, _newValue) => this.onChangeDarkMode());
this.subscribe('position', (_oldPosition, newPosition) => this.onPositionChanged(newPosition));
this.subscribe('position.scale', (_oldScale, newScale) => this.onChangeScale(newScale));
this.subscribe('position.resolution', (_oldResolution, newResolution) => this.zoomToResolution(newResolution));
this.subscribe('position.zoom', (_oldZoom, newZoom) => this.zoomToZoom(newZoom));
this.subscribe('position.center', (_oldCenter, newCenter) => this.panToCoordinate(newCenter));
this.subscribe('selection.selectionParameters', (_oldParams, newParams) => this.onSelectFeatures(newParams));
this.subscribe('selection.selectedFeatures', (_oldFeatures, newFeatures) => this.onFeaturesSelected(newFeatures));
this.subscribe('selection.highlightedFeatures', (_oldFeatures, newFeatures) => this.onFeatureHighlighted(newFeatures));
this.subscribe('selection.focusedFeatures', (_oldFeature, newFeature) => this.focusFeature.setFocusedFeatures(newFeature));
this.subscribe('globe.display', () => this.onGlobeToggled());
this.subscribe('globe.shadows', (_oldShadows, newShadows) => this.onShadowsToggled(newShadows));
this.subscribe('globe.shadowsTimestamp', (_oldTimestamp, newTimestamp) => this.onShadowsTimestampChanged(newTimestamp));
this.subscribe(/layers\.layersList\..*\.activeState/, (_oldActive, _newActive, layer) => this.onLayerToggled(layer));
this.subscribe(/layers\.layersList\..*\.opacity/, (_oldOpacity, _newOpacity, layer) => this.onChangeOpacity(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('sharedStateIsLoaded', (_, isLoaded) => {
if (isLoaded) {
// The map component can be loaded after the initialization of the shared state.
// And then, the callbacks from previous subscribes have perhaps not be called, because this component did not exist yet.
// Therefore, if there is a shared state, we have to initialize all the layers and position manually.
// TODO REG : Find another solution for this, because this could happen in other contexts.
this.onPositionChanged(this.state.position);
this.activateSharedLayers(this.state.layers.layersList);
this.setMapPositionFromUrlAfterInit();
}
});
}
activateSharedLayers(layers) {
for (const layer of layers) {
// Activate the layer by default
if (layer instanceof Layer && layer.isDefaultChecked) {
this.onLayerToggled(layer);
this.onChangeOpacity(layer);
this.onChangeFilter(layer);
this.onChangeTime(layer);
}
// Continue recursively
if (layer instanceof GroupLayer || layer instanceof ThemeLayer) {
this.activateSharedLayers(layer.children);
}
}
}
render() {
super.render();
// Read out the default projection from config, ignoring user preferences
this.defaultSrid = this.configManager.getDefaultConfigValue('map.srid');
this.setMapPositionFromUrlAfterInit();
// 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.wmsManager = WmsManager.getInstance();
this.wmsManager.map = this.olMap;
this.osmManager = new OsmManager(this.olMap);
this.cogManager = new CogManager(this.olMap);
this.xyzManager = new XyzManager(this.olMap);
this.viewManager = new ViewManager(this.olMap);
this.vectorTilesManager = new VectorTilesManager(this.olMap);
this.localFileManager = new LocalFileManager(this.olMap);
this.wmtsManager = new WmtsManager(this.olMap);
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.wmsManager, this.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.configManager.loadConfig().then(() => {
this.selectionLayer = new VectorLayer({
properties: {
addToPrintedLayers: true
},
source: selectionSource
});
this.highlightLayer = new VectorLayer({
source: highlightSource
});
this.setSelectLayerStyle();
this.setHighlightLayerStyle();
this.olMap.addLayer(this.selectionLayer);
this.olMap.addLayer(this.highlightLayer);
this.selectionLayer.setZIndex(1002);
this.highlightLayer.setZIndex(1003);
this.selectionLayer.set('altitudeMode', 'clampToGround');
if (this.configManager.Config.map.showScaleLine) {
const scaleLine = new ScaleLine({
units: 'metric'
});
this.olMap.addControl(scaleLine);
}
});
// TODO REG: This is ugly, but I didn't find any other solution yet.
setTimeout(() => {
this.olMap.updateSize();
}, 1000);
}
/**
* Add a click handler to hide the popup.
*/
closePopup() {
this.tooltipOverlay.setPosition(undefined);
this.tooltipContainer.classList.add('hidden');
}
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.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;
}
onMoveEnd(_e) {
const view = this.olMap.getView();
const newPosition = new MapPosition();
newPosition.center = view.getCenter();
newPosition.zoom = view.getZoom();
newPosition.resolution = view.getResolution();
newPosition.scale = this.viewManager.getScale();
newPosition.tooltip = this.state.position.tooltip;
if (newPosition.isValid) {
this.state.position = newPosition;
}
}
onClick(e) {
// Build selection box using the default tolerance.
const topLeftPixel = [e.pixel[0] - this.pixelTolerance, e.pixel[1] - this.pixelTolerance];
const topLeftCoord = this.olMap.getCoordinateFromPixel(topLeftPixel);
const bottomRightPixel = [e.pixel[0] + this.pixelTolerance, e.pixel[1] + this.pixelTolerance];
const bottomRightCoord = this.olMap.getCoordinateFromPixel(bottomRightPixel);
this.select([topLeftCoord[0], topLeftCoord[1], bottomRightCoord[0], bottomRightCoord[1]]);
}
onDragSelection(_e) {
const extent = this.dragbox.getGeometry().getExtent();
this.select(extent);
}
select(extent) {
// Reset current selection
this.state.selection.selectedFeatures = [];
this.state.selection.selectionParameters = [];
this.state.selection.highlightedFeatures = [];
// Layers selectable today are WMS, WMTS (with wms layer) and Local files
this.wmsManager.selectFeatures(extent);
this.wmtsManager.selectFeatures(extent);
this.localFileManager.selectFeatures(extent);
}
async onSelectFeatures(selectionParams) {
this.state.loading = true;
// WMS GetFeatureInfo
const wmsPromises = selectionParams.map((param) => {
const wmsGetFeatureInfoSelectionParam = param.clone((l) => l.queryable && !l.wfsQueryable);
const client = this.wmsManager.getClient(wmsGetFeatureInfoSelectionParam._ogcServer);
return client.getFeatureInfo(wmsGetFeatureInfoSelectionParam);
});
// WFS GetFeature
const wfsPromises = selectionParams.map((param) => {
const wfsGetFeatureInfoSelectionParam = param.clone((l) => l.wfsQueryable);
const client = WfsManager.getInstance().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 {
this.state.selection.selectedFeatures.push(...gmlFeatures);
this.state.interface.selectionComponentVisible = true;
}
this.state.loading = false;
}
connectedCallback() {
this.loadConfig().then(() => {
this.render();
this.listenOpenLayersEvents();
this.registerEvents();
});
}
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 = parseFloat(this.swiper.value);
const max = parseFloat(this.swiper.max);
const min = parseFloat(this.swiper.min);
const percent = (sliderValue - min) / (max - min);
const offset = percent * this.swiper.offsetWidth;
this.closeSwiperButton.style.left = `${offset}px`;
}
setSelectLayerStyle() {
this.selectionLayer.setStyle(new Style({
stroke: new Stroke({
color: this.configManager.Config.selection.defaultStrokeColor,
width: this.configManager.Config.selection.defaultStrokeWidth
}),
fill: new Fill({ color: this.configManager.Config.selection.defaultFillColor }),
image: new Circle({
radius: 7,
fill: new Fill({ color: this.configManager.Config.selection.defaultFillColor }),
stroke: new Stroke({
color: this.configManager.Config.selection.defaultStrokeColor,
width: this.configManager.Config.selection.defaultStrokeWidth
})
})
}));
}
setHighlightLayerStyle() {
this.highlightLayer.setStyle(new Style({
stroke: new Stroke({
color: this.configManager.Config.selection.highlightStrokeColor,
width: this.configManager.Config.selection.defaultStrokeWidth
}),
fill: new Fill({ color: this.configManager.Config.selection.highlightFillColor }),
image: new Circle({
radius: 7,
fill: new Fill({ color: this.configManager.Config.selection.highlightFillColor }),
stroke: new Stroke({
color: this.configManager.Config.selection.highlightStrokeColor,
width: this.configManager.Config.selection.defaultStrokeWidth
})
})
}));
}
setCrosshairStyle() {
this.crosshairLayer.setStyle(new Style({
image: new RegularShape({
fill: new Fill({ color: this.configManager.Config.selection.defaultFillColor }),
stroke: new Stroke({ color: this.configManager.Config.selection.defaultStrokeColor, width: 2 }),
points: 4,
radius: 10,
radius2: 0,
angle: 0
})
}));
}
async create3dMap() {
if (!this.map3d && this.configManager.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 isNaN(date.getTime()) ? Cesium.JulianDate.now() : Cesium.JulianDate.fromDate(date);
}
});
const scene = this.map3d.getCesiumScene();
const config = this.configManager.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));
});
const ambientOcclusion = scene.postProcessStages.ambientOcclusion;
ambientOcclusion.enabled = true;
ambientOcclusion.uniforms.bias = 0.5;
ambientOcclusion.uniforms.stepSize = 1;
ambientOcclusion.uniforms.blurStepSize = 1;
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.configManager.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.state.layers.layersList.forEach((l) => this.addAllActiveLayers3dMap(l));
}
}
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();
}
onPositionChanged(position) {
this.zoomToResolution(position.resolution);
if (position.isValid) {
this.panToCoordinate(position.center);
this.updateUrlWithMapPosition();
}
}
onChangeScale(scale) {
this.viewManager.setScale(scale);
}
zoomToResolution(resolution) {
this.viewManager.setResolution(resolution);
}
zoomToZoom(zoom) {
this.viewManager.setZoom(zoom);
}
zoomToExtent(extent) {
this.olMap.getView().fit(extent);
}
panToCoordinate(coordinate) {
this.viewManager.setCenter(coordinate);
}
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');
}
}
onLayerToggled(layer) {
if (layer instanceof Layer) {
if (layer.active) {
this.onAddLayers([layer]);
}
else {
this.onRemoveLayers([layer]);
}
}
}
onAddLayers(layerInfos) {
layerInfos.forEach((l) => {
if (l instanceof LayerWms) {
this.wmsManager.getClient(l).addLayer(l);
if (this.wmsManager3d != null)
this.wmsManager3d.addLayer(l);
}
else if (l instanceof LayerWmts) {
this.wmtsManager.addLayer(l);
}
else if (l instanceof LayerLocalFile) {
this.localFileManager.addLayer(l);
}
else if (l instanceof LayerCog) {
this.cogManager.addLayer(l);
}
else if (l instanceof LayerXYZ) {
this.xyzManager.addLayer(l);
}
});
}
onRemoveLayers(layerInfos) {
layerInfos.forEach((l) => {
if (l instanceof LayerWms) {
this.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.localFileManager.removeLayer(l);
}
else if (l instanceof LayerCog) {
this.cogManager.removeLayer(l);
}
else if (l instanceof LayerXYZ) {
this.xyzManager.removeLayer(l);
}
});
}
reorderLayers() {
console.log('ORDER CHANGED FOR MAP');
this.wmtsManager.refreshZIndexes();
this.wmsManager.refreshZIndexes();
}
onChangeOpacity(layerInfos) {
if (layerInfos instanceof LayerWms) {
this.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, layerInfos.opacity);
}
}
}
onChangeFilter(layerInfos) {
if (layerInfos instanceof LayerWms) {
this.wmsManager.getClient(layerInfos).changeFilter(layerInfos);
if (this.wmsManager3d != null)
this.wmsManager3d.changeFilter(layerInfos);
}
}
onChangeTime(layer) {
if (layer instanceof LayerWms && layer.active) {
this.wmsManager.getClient(layer).changeTimeRestriction(layer);
}
else if (layer instanceof GroupLayer) {
// Apply the time restriction to all children of the group layers
for (const childLayer of layer.children) {
if (isTimeAwareLayer(childLayer) && childLayer.timeRestriction !== layer.timeRestriction) {
childLayer.timeRestriction = layer.timeRestriction;
}
}
}
}
onChangeBasemap(basemap) {
// First, remove all existing basemaps
this.wmtsManager.removeAllBasemapLayers();
this.wmsManager.removeAllBasemapLayers();
if (this.wmsManager3d != null)
this.wmsManager3d.removeAllBasemapLayers();
this.osmManager.removeAllBasemapLayers();
this.cogManager.removeAllBasemapLayers();
this.xyzManager.removeAllBasemapLayers();
this.vectorTilesManager.removeAllBasemapLayers();
// Then, add the selected basemaps
for (const layer of 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.wmsManager.getClient(layer).addBasemapLayer(layer);
if (this.wmsManager3d != null)
this.wmsManager3d.addBasemapLayer(layer);
}
else {
throw new Error('Unknown basemap type');
}
}
}
// TODO REG: Move the 3 following methods to a dedicated manager to manage URL status globaly?
/**
* Add the current map position and zoom level to the URL. The coordinates are provided in
* the default reference system or in WGS84.
*/
updateUrlWithMapPosition() {
let mapX = this.state.position.center[0];
let mapY = this.state.position.center[1];
if (!mapX || !mapY) {
return;
}
if (!isProjectionInDegrees()) {
// Round to meters
mapX = Math.round(mapX);
mapY = Math.round(mapY);
}
const currentPosition = new MapPosition();
currentPosition.center = [mapX, mapY];
currentPosition.zoom = this.state.position.zoom;
// Transform position if it's not in the default reference system or WGS84
if (this.state.projection !== this.defaultSrid && this.state.projection !== 'EPSG:4326') {
currentPosition.center = transform(currentPosition.center, this.projection, 'EPSG:4326');
}
setUrlFromMapPosition(currentPosition);
}
/**
* Reads the map position data from the URL and applies it to the map,
* making sure map initialization and loading of the shared state have finished beforehand.
*/
setMapPositionFromUrlAfterInit() {
if (!this.initialPositionFromUrl) {
return;
}
this.state.position = this.initialPositionFromUrl;
if (!this.state.projection || !this.olMap.getView().getResolution()) {
// Everything os not ready yet. Delay the execution of this method on rendercomplete
this.olMap.once('rendercomplete', () => {
this.applyMapPositionFromUrl(this.initialPositionFromUrl);
});
}
else {
this.applyMapPositionFromUrl(this.initialPositionFromUrl);
}
}
/**
* Updates the map position based on a provided position from the URL. Ensures the position is transformed
* to the correct map reference system and applies it to the map view if valid.
* @param {MapPosition} position - The map position object, including center coordinates in the default reference
* system or WGS84, and zoom level.
*/
applyMapPositionFromUrl(position) {
// Make sure the map has finished initializing
if (!this.state.projection || !this.olMap.getView().getResolution()) {
return;
}
// Transform position into current map reference system
const projectionInUrl = getProjection(isCoordinateInDegrees(position.center) ? 'EP