@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
300 lines (299 loc) • 18.2 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
import GeoGirafeAppComponent from './geogirafe-component.js';
import tippy from 'tippy.js';
import AboutComponent from '../../components/about/component.js';
import BasemapComponent from '../../components/basemap/component.js';
import CoordinateComponent from '../../components/coordinate/component.js';
import ContactComponent from '../../components/contact/component.js';
import MapDefaultContextMenuComponent from '../../components/context-menu/default-context-menu/component.js';
import MapCustomContextMenuComponent from '../../components/context-menu/custom-context-menu/component.js';
import CrossSectionSettingsComponent from '../../components/cross-section/cross-section-settings/component.js';
import CrossSectionViewComponent from '../../components/cross-section/cross-section-viewer/component.js';
import DrawingComponent, { DrawingStateLocation } from '../../components/drawing/component.js';
import EditComponent from '../../components/edit/component.js';
import EditFromComponent from '../../components/edit/editform/component.js';
import HelpComponent from '../../components/help/component.js';
import InfoboxComponent from '../../components/infobox/component.js';
import InfoWindowComponent from '../../components/infowindow/component.js';
import LayoutComponent from '../../components/layout/component.js';
import LRPanelComponent from '../../components/lr-panel/component.js';
import Map2dComponent from '../../components/map-2d/component.js';
import DeprecatedMapComponent from '../../components/map-2d/deprecatedmapcomponent.js';
import Map3dComponent from '../../components/map-3d/component.js';
import GoogleStreetViewComponent from '../../components/streetview/google.js';
import MapGridComponent from '../../components/map-grid/component.js';
import MenuButtonComponent from '../../components/menubutton/component.js';
import ModalsComponent from '../../components/modals/component.js';
import NavigationComponent from '../../components/navigation/component.js';
import NewsButtonComponent from '../../components/news/news-button/component.js';
import NewsPanelComponent from '../../components/news/news-panel/component.js';
import OauthComponent from '../../components/auth/component.js';
import PrintComponent from '../../components/print/component.js';
import PrototypeBannerComponent from '../../components/prototypebanner/component.js';
import QueryBuilderComponent from '../../components/querybuilder/component.js';
import ScaleComponent from '../../components/scale/component.js';
import SearchComponent from '../../components/search/component.js';
import SelectionGridComponent from '../../components/selectiongrid/component.js';
import SelectionWindowComponent from '../../components/selectionwindow/component.js';
import ShareComponent from '../../components/share/component.js';
import ExternalLayersComponent from '../../components/extlayers/component.js';
import ThemeComponent from '../../components/themes/component.js';
import TimeRestrictionComponent from '../../components/timerestriction/component.js';
import TimePickerComponent from '../../components/timerestriction/timepicker/component.js';
import TimeSliderComponent from '../../components/timerestriction/timeslider/component.js';
import TreeViewGroupComponent from '../../components/treeview/treeviewgroup/component.js';
import TreeViewItemComponent from '../../components/treeview/treeviewitem/component.js';
import TreeViewRootComponent from '../../components/treeview/treeviewroot/component.js';
import TreeViewThemeComponent from '../../components/treeview/treeviewtheme/component.js';
import UserPreferencesComponent from '../../components/userpreferences/component.js';
import VideoRecordComponent from '../../components/videorecord/component.js';
import DrawingState from '../drawing/drawingState.js';
import DrawingSerializer from '../drawing/drawingSerializer.js';
import { ShareState, ShareStateSerializer } from '../../components/share/sharestate.js';
import SelectionToolComponent from '../../components/selectiontool/component.js';
import AdvancedFilterComponent from '../../components/advancedfilter/component.js';
import FixedDimensionComponent from '../../components/drawing/fixed-dimension/component.js';
import GetDirectionsArtifact from '../../components/getdirections/component.js';
import ServiceWorkerHelper from '../utils/swhelper.js';
import FeedbackButtonComponent from '../../components/feedbackbutton/component.js';
import AddBookmarkComponent from '../../components/addbookmark/component.js';
import DrawingToolbarComponent from '../../components/drawing-toolbar/component.js';
import SearchFilterComponent from '../../components/search-filter/component.js';
import HeaderComponent from '../../components/header/component.js';
export default class GeoGirafeApp {
readyPromise;
resolveReady;
mainComponent;
isIframe;
get context() {
return this.mainComponent.getContext();
}
constructor(isIframe = false) {
this.isIframe = isIframe;
this.readyPromise = new Promise((resolve) => {
this.resolveReady = resolve;
});
this.initialize();
}
async isReady() {
return this.readyPromise;
}
async initialize() {
this.defineMainComponent();
await this.mainComponent.isReady();
await this.initializeServiceWorker();
this.initializeInterface();
this.addCustomSerializers();
this.context.stateManager.state.application.isCustomSerializerInitialized = true;
this.defineCoreComponents();
this.resolveReady();
}
defineMainComponent() {
// Define custom element for the main application
customElements.define('girafe-app', GeoGirafeAppComponent);
this.mainComponent = document.querySelector('girafe-app');
if (!this.mainComponent) {
throw new Error('Application GeoGirafe not found. Please verify that the component is present in the HTML.');
}
}
initializeInterface() {
// Override default tooltip maxWidth:
tippy.setDefaultProps({ maxWidth: '' });
}
addCustomSerializers() {
// Add custom state and serializers (need to be done early, because the shared state will need them)
this.context.stateManager.state.extendedState[DrawingStateLocation] = new DrawingState();
this.context.stateSerializer.addSerializer(DrawingState, new DrawingSerializer(this.context));
this.context.stateManager.state.extendedState.share = new ShareState();
this.context.stateSerializer.addSerializer(ShareState, new ShareStateSerializer(this.context, this.isIframe));
}
defineCoreComponents() {
customElements.define('girafe-about', AboutComponent);
customElements.define('girafe-advanced-filter', AdvancedFilterComponent);
customElements.define('girafe-header', HeaderComponent);
customElements.define('girafe-basemap', BasemapComponent);
customElements.define('girafe-coordinate', CoordinateComponent);
customElements.define('girafe-contact', ContactComponent);
customElements.define('girafe-default-context-menu', MapDefaultContextMenuComponent);
customElements.define('girafe-custom-context-menu', MapCustomContextMenuComponent);
customElements.define('girafe-layout', LayoutComponent);
customElements.define('girafe-help', HelpComponent);
customElements.define('girafe-infobox', InfoboxComponent);
customElements.define('girafe-info-window', InfoWindowComponent);
customElements.define('girafe-cross-section-settings', CrossSectionSettingsComponent);
customElements.define('girafe-cross-section-view', CrossSectionViewComponent);
customElements.define('girafe-lr-panel', LRPanelComponent);
customElements.define('girafe-map-2d', Map2dComponent);
customElements.define('girafe-map-3d', Map3dComponent);
customElements.define('girafe-streetview-google', GoogleStreetViewComponent);
customElements.define('girafe-map-grid', MapGridComponent);
// Deprecated alias of <girafe-map-2d>, kept for the applications that were not migrated yet.
customElements.define('girafe-map', DeprecatedMapComponent);
customElements.define('girafe-menu-button', MenuButtonComponent);
customElements.define('girafe-modals', ModalsComponent);
customElements.define('girafe-nav-history', NavigationComponent);
customElements.define('girafe-news-button', NewsButtonComponent);
customElements.define('girafe-news', NewsPanelComponent);
customElements.define('girafe-print', PrintComponent);
customElements.define('girafe-prototype-banner', PrototypeBannerComponent);
customElements.define('girafe-query-builder', QueryBuilderComponent);
customElements.define('girafe-oauth', OauthComponent);
customElements.define('girafe-drawing', DrawingComponent);
customElements.define('girafe-drawing-toolbar', DrawingToolbarComponent);
customElements.define('girafe-edit', EditComponent);
customElements.define('girafe-edit-form', EditFromComponent);
customElements.define('girafe-scale', ScaleComponent);
customElements.define('girafe-search-filter', SearchFilterComponent);
customElements.define('girafe-search', SearchComponent);
customElements.define('girafe-selection-tool', SelectionToolComponent);
customElements.define('girafe-selection-grid', SelectionGridComponent);
customElements.define('girafe-selection-window', SelectionWindowComponent);
customElements.define('girafe-share', ShareComponent);
customElements.define('girafe-ext-layer', ExternalLayersComponent);
customElements.define('girafe-theme-select', ThemeComponent);
customElements.define('girafe-time-restriction', TimeRestrictionComponent);
customElements.define('girafe-time-picker', TimePickerComponent);
customElements.define('girafe-time-slider', TimeSliderComponent);
customElements.define('girafe-tree-view', TreeViewRootComponent);
customElements.define('girafe-tree-view-group', TreeViewGroupComponent);
customElements.define('girafe-tree-view-item', TreeViewItemComponent);
customElements.define('girafe-tree-view-theme', TreeViewThemeComponent);
customElements.define('girafe-user-preferences', UserPreferencesComponent);
customElements.define('girafe-video-record', VideoRecordComponent);
customElements.define('girafe-fixed-dimension', FixedDimensionComponent);
customElements.define('girafe-get-directions', GetDirectionsArtifact);
customElements.define('girafe-feedback-button', FeedbackButtonComponent);
customElements.define('girafe-add-bookmark', AddBookmarkComponent);
}
async initializeServiceWorker() {
try {
const storeVersion = 6;
const dbCacheName = 'geogirafe-cache';
if (globalThis.location.protocol !== 'https:') {
throw new Error('Service worker registration not possible if the application is served over HTTP.');
}
if (!navigator?.serviceWorker) {
throw new Error('Service worker not supported by your browser.');
}
let sw;
try {
sw = await this.waitForServiceWorkerControl();
}
catch (err) {
throw new Error(`Service worker registration failed. ${err}`, { cause: err });
}
const config = this.context.configManager.Config;
const serviceWorkerHelper = new ServiceWorkerHelper(sw);
// Communicate configuration to service-worker
// NOTE REG : Important : we have to wait that the config has been correctly sent to the SW
// Otherwise the first requests can be sent without the correct auhentication.
const issuerConfig = config.oauth?.issuer ?? config.gmfauth;
const gmfConfig = config.oauth?.geomapfish ?? config.gmfauth;
if (issuerConfig && gmfConfig) {
const issuerHostname = new URL(issuerConfig.url).hostname;
const audience = [...issuerConfig.audience, issuerHostname];
const swConfig = {
logLevel: config.general.logLevel,
audience: audience,
audienceExcludedPaths: issuerConfig.audienceExcludedPaths,
authMode: gmfConfig.authMode,
alwaysSendCookies: issuerConfig.alwaysSendCookies,
refererPolicy: gmfConfig.refererPolicy
};
await serviceWorkerHelper.sendMessageToServiceWorker(swConfig);
}
else {
// If no auth, we just update the log level, but we do not have to wait
sw.postMessage({ logLevel: config.general.logLevel });
}
await this.context.offlineManager.setServiceWorker(sw, storeVersion, dbCacheName);
await this.context.authManager.initialize(sw);
}
catch (e) {
console.error("Service worker could not be initialized. Authentication and offline maps won't work.\n", e);
// Finish auth initialization, since without a service worker auth isn't possible
this.context.stateManager.state.application.isAuthInitialized = true;
}
}
async waitForServiceWorkerControl() {
const sw = navigator.serviceWorker;
const registration = await this.registerServiceWorker();
// If a service worker already controls the current page, everything is ready.
// `registration.active` only tells us that a service worker is active for the registration;
// `sw.controller` tells us that it actually controls this page.
if (sw.controller) {
return sw.controller;
}
// If the page is still outside of the service worker scope (see registerServiceWorker),
// the `controllerchange` event will never be fired, so we only wait for the service worker to be activated.
if (!globalThis.location.href.startsWith(registration.scope)) {
console.warn(`The current page is outside of the service worker scope (${registration.scope}), it won't be controlled by the service worker.`);
return this.waitForServiceWorkerActivation(registration);
}
// Wait until the service worker takes control of the current page.
// Once `clients.claim()` succeeds, the browser updates `sw.controller` and
// dispatches the `controllerchange` event.
return new Promise((resolve) => {
sw.addEventListener('controllerchange', () => {
const controller = navigator.serviceWorker.controller;
if (controller) {
resolve(controller);
}
}, { once: true });
// The page is not currently controlled by a service worker.
// This can happen, for example, after a force refresh (Ctrl+F5), even though
// an active service worker already exists. In that case, the service worker's
// `activate` event is not triggered again, so the `clients.claim()` call from
// the activation phase cannot take control of this newly loaded page.
// If an active service worker already exists, explicitly ask it to claim the
// current page. The service worker handles this message by calling `clients.claim()`.
const active = registration.active;
if (active) {
active.postMessage({ type: 'claim' });
}
// Safety check: This is exactly the same test as the begining of the method
// but the worker may have claimed the page before the listener
// above was registered, in which case `controllerchange` never fires.
if (sw.controller) {
resolve(sw.controller);
}
});
}
async registerServiceWorker() {
// The default scope of the service worker is the folder of service-worker.js (resolved against <base href>).
// When the application page is served from another path than its static files
// (e.g. page at /iframe and files at /static/), the page is outside of this scope and can't be controlled.
// In that case, try to register the service worker at path of the page as scope.
// This is only allowed by the browser if the server sends the "Service-Worker-Allowed" header.
const scriptUrl = new URL('service-worker.js', document.baseURI);
const defaultScope = new URL('./', scriptUrl).href;
if (!globalThis.location.href.startsWith(defaultScope)) {
const pageScope = new URL('./', globalThis.location.href).href;
try {
return await navigator.serviceWorker.register(scriptUrl, { type: 'module', scope: pageScope });
}
catch (err) {
console.warn(`Service worker could not be registered with the scope ${pageScope}. ` +
`Check that the server sends the header "Service-Worker-Allowed" with the service worker script.`, err);
}
}
return navigator.serviceWorker.register(scriptUrl, { type: 'module' });
}
async waitForServiceWorkerActivation(registration) {
if (registration.active) {
return registration.active;
}
const sw = registration.installing ?? registration.waiting;
if (!sw) {
throw new Error('No service worker is installing or waiting.');
}
return new Promise((resolve) => {
sw.addEventListener('statechange', function onStateChange() {
if (sw.state === 'activated') {
sw.removeEventListener('statechange', onStateChange);
resolve(sw);
}
});
});
}
}