UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

748 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.s