UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

993 lines (991 loc) 50.9 kB
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