@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
748 lines • 67 kB
JavaScript
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