- ${this.createMode || !isProject(project) ? '' : html`
-
- ${`${i18next.t('dashboard_modified_title')} ${toLocaleDateString(project.modified)} ${i18next.t('dashboard_by_swisstopo_title')}`}
-
+ ${project.views.map((view, index) => html`
+
+
+ view.title}
+ @input=${evt => {
+ view.title = evt.target.value;
+ this.requestUpdate();
+ }}/>
+ ${i18next.t('project_view')}
-
-
+
+
+
+ {
+ project!.assets = evt.detail.assets;
+ this.project = {...project};
+ }}">
+
+
+
this.onMemberAdd(evt)}
+ @onMemberDelete=${evt => this.onMemberDelete(evt)}>
+
+
this.dispatchEvent(new CustomEvent('onBack'))}>
+
+ ${i18next.t('dashboard_back_to_topics')}
+
+
+
+
+
`;
- }
+ }
- async saveViewToProject() {
- if (!this.project) return;
- const project = {...this.project};
- const view: View = {
- id: crypto.randomUUID(),
- title: `${i18next.t('view')} ${project.views.length + 1}`,
- permalink: getPermalink(),
- };
- project.views.push(view);
- this.project = project;
- }
+ async saveViewToProject() {
+ if (!this.project) return;
+ const project = {...this.project};
+ const view: View = {
+ id: crypto.randomUUID(),
+ title: `${i18next.t('view')} ${project.views.length + 1}`,
+ permalink: getPermalink(),
+ };
+ project.views.push(view);
+ this.project = project;
+ }
- createRenderRoot() {
- return this;
- }
+ createRenderRoot() {
+ return this;
+ }
}
function array_move(arr, old_index, new_index) {
- if (new_index >= arr.length) {
- let k = new_index - arr.length + 1;
- while (k--) {
- arr.push(undefined);
- }
+ if (new_index >= arr.length) {
+ let k = new_index - arr.length + 1;
+ while (k--) {
+ arr.push(undefined);
}
- arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
-}
\ No newline at end of file
+ }
+ arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
+}
diff --git a/ui/src/elements/dashboard/ngm-project-topic-overview.ts b/ui/src/elements/dashboard/ngm-project-topic-overview.ts
index be63b2ebf..d16cd36df 100644
--- a/ui/src/elements/dashboard/ngm-project-topic-overview.ts
+++ b/ui/src/elements/dashboard/ngm-project-topic-overview.ts
@@ -6,7 +6,7 @@ import {classMap} from 'lit/directives/class-map.js';
import {styleMap} from 'lit/directives/style-map.js';
import DashboardStore from '../../store/dashboard';
import {CreateProject, Project, TabTypes, Topic, type View} from './ngm-dashboard';
-import {apiClient} from '../../api-client';
+import {ApiClient} from '../../api/api-client';
import {showBannerSuccess} from '../../notifications';
import $ from '../../jquery';
import {DEFAULT_PROJECT_COLOR} from '../../constants';
@@ -17,69 +17,76 @@ import './ngm-project-members-section';
import {isProject} from './helpers';
import {NgmConfirmationModal} from '../ngm-confirmation-modal';
import {getPermalink} from '../../permalink';
+import {consume} from '@lit/context';
+import {apiClientContext} from '../../context';
@customElement('ngm-project-topic-overview')
export class NgmProjectTopicOverview extends LitElementI18n {
- @property({type: Object})
- accessor topicOrProject: Project | Topic | undefined;
- @property({type: Object})
- accessor toastPlaceholder: HTMLElement | undefined;
- @property({type: String})
- accessor activeTab: TabTypes = 'topics';
- @property({type: String})
- accessor userEmail: string = '';
- @property({type: Number})
- accessor selectedViewIndx: number | undefined;
- @query('ngm-confirmation-modal')
- accessor deleteWarningModal!: NgmConfirmationModal;
+ @property({type: Object})
+ accessor topicOrProject: Project | Topic | undefined;
+ @property({type: Object})
+ accessor toastPlaceholder: HTMLElement | undefined;
+ @property({type: String})
+ accessor activeTab: TabTypes = 'topics';
+ @property({type: String})
+ accessor userEmail: string = '';
+ @property({type: Number})
+ accessor selectedViewIndx: number | undefined;
+ @query('ngm-confirmation-modal')
+ accessor deleteWarningModal!: NgmConfirmationModal;
- shouldUpdate(_changedProperties: PropertyValues): boolean {
- return this.topicOrProject !== undefined;
- }
+ @consume({context: apiClientContext})
+ accessor apiClient!: ApiClient;
- firstUpdated(_changedProperties: PropertyValues) {
- this.querySelectorAll('.ui.dropdown').forEach(elem => $(elem).dropdown());
- super.firstUpdated(_changedProperties);
- }
+ shouldUpdate(_changedProperties: PropertyValues): boolean {
+ return this.topicOrProject !== undefined;
+ }
+
+ firstUpdated(_changedProperties: PropertyValues) {
+ this.querySelectorAll('.ui.dropdown').forEach(elem => $(elem).dropdown());
+ super.firstUpdated(_changedProperties);
+ }
- render() {
- if (!this.topicOrProject) return '';
- const project = isProject(this.topicOrProject) ? this.topicOrProject : undefined;
- const ownerEmail = project?.owner?.email;
- const owner = ownerEmail || i18next.t('swisstopo');
- const date = this.topicOrProject?.modified ? this.topicOrProject?.modified : this.topicOrProject?.created;
- const backgroundImage = this.topicOrProject.image?.length ? `url('${this.topicOrProject.image}')` : 'none';
- const editorEmails = project?.editors?.map(m => m.email) || [];
- const projectModerator = [ownerEmail, ...editorEmails].includes(this.userEmail);
+ render() {
+ if (!this.topicOrProject) return '';
+ const project = isProject(this.topicOrProject) ? this.topicOrProject : undefined;
+ const ownerEmail = project?.owner?.email;
+ const owner = ownerEmail || i18next.t('swisstopo');
+ const date = this.topicOrProject?.modified ? this.topicOrProject?.modified : this.topicOrProject?.created;
+ const backgroundImage = this.topicOrProject.image?.length ? `url('${this.topicOrProject.image}')` : 'none';
+ const editorEmails = project?.editors?.map(m => m.email) || [];
+ const projectModerator = [ownerEmail, ...editorEmails].includes(this.userEmail);
- return html`
+ return html`
this.deleteProject()}"
.text="${{
- title: i18next.t('dashboard_delete_warning_title'),
- description: i18next.t('dashboard_delete_warning_description'),
- cancelBtn: i18next.t('cancel'),
- confirmBtn: i18next.t('delete'),
+ title: i18next.t('dashboard_delete_warning_title'),
+ description: i18next.t('dashboard_delete_warning_description'),
+ cancelBtn: i18next.t('cancel'),
+ confirmBtn: i18next.t('delete'),
}}"
- >
+ >
${translated(this.topicOrProject.title)}
${
- `${this.topicOrProject.modified ? i18next.t('modified_on') : i18next.t('created_on')} ${toLocaleDateString(date)} ${i18next.t('by')} ${owner}`
- }
+ `${this.topicOrProject.modified ? i18next.t('modified_on') : i18next.t('created_on')} ${toLocaleDateString(date)} ${i18next.t('by')} ${owner}`
+ }
+
${this.topicOrProject.views.map((view, index) => html`
@@ -123,16 +131,16 @@ export class NgmProjectTopicOverview extends LitElementI18n {
-
-
+
+
${!project ? '' : html`
-
-
+
+
`}
this.dispatchEvent(new CustomEvent('onDeselect'))}>
@@ -140,16 +148,16 @@ export class NgmProjectTopicOverview extends LitElementI18n {
${i18next.t('dashboard_back_to_topics')}
`;
- }
+ }
- contextMenu() {
- return html`
+ contextMenu() {
+ return html`
`;
- }
+ }
- async duplicateToProject() {
- const createProject = this.toCreateProject(this.topicOrProject!);
- const response = await apiClient.duplicateProject(createProject);
- const id = await response.json();
- const project = await apiClient.getProject(id);
- this.dispatchEvent(new CustomEvent('onProjectDuplicated', {detail: {project}}));
- }
+ async duplicateToProject() {
+ const createProject = this.toCreateProject(this.topicOrProject!);
+ const response = await this.apiClient.duplicateProject(createProject);
+ const id = await response.json();
+ const project = await this.apiClient.getProject(id);
+ this.dispatchEvent(new CustomEvent('onProjectDuplicated', {detail: {project}}));
+ }
- async deleteProject() {
- await apiClient.deleteProject(this.topicOrProject!.id);
- }
+ async deleteProject() {
+ await this.apiClient.deleteProject(this.topicOrProject!.id);
+ }
- async copyLink(viewId?: string) {
- try {
- const link = this.getLink(viewId);
- if (link) await navigator.clipboard.writeText(link);
- showBannerSuccess(this.toastPlaceholder!, i18next.t('shortlink_copied'));
- } catch (e) {
- console.error(e);
- }
+ async copyLink(viewId?: string) {
+ try {
+ const link = this.getLink(viewId);
+ if (link) await navigator.clipboard.writeText(link);
+ showBannerSuccess(this.toastPlaceholder!, i18next.t('shortlink_copied'));
+ } catch (e) {
+ console.error(e);
}
+ }
- getLink(viewId?: string): string | undefined {
- if (!this.topicOrProject) return;
- let link = `${location.protocol}//${location.host}${location.pathname}?`;
- const idKey = isProject(this.topicOrProject) ? 'projectId' : 'topicId';
- link = `${link}${idKey}=${this.topicOrProject.id}`;
- if (viewId) link = `${link}&viewId=${viewId}`;
- return link;
- }
+ getLink(viewId?: string): string | undefined {
+ if (!this.topicOrProject) return;
+ let link = `${location.protocol}//${location.host}${location.pathname}?`;
+ const idKey = isProject(this.topicOrProject) ? 'projectId' : 'topicId';
+ link = `${link}${idKey}=${this.topicOrProject.id}`;
+ if (viewId) link = `${link}&viewId=${viewId}`;
+ return link;
+ }
- toCreateProject(topicOrProject: Topic | Project): CreateProject {
- const title = isProject(topicOrProject) ?
- `${i18next.t('tbx_copy_of_label')} ${topicOrProject.title}` :
- translated(topicOrProject.title);
- let description: string | undefined;
- if (isProject(topicOrProject)) {
- description = topicOrProject.description;
- } else if (topicOrProject.description) {
- description = translated(topicOrProject.description);
- }
- return {
- title,
- description,
- color: isProject(topicOrProject) ? topicOrProject.color : DEFAULT_PROJECT_COLOR,
- geometries: isProject(topicOrProject) ? topicOrProject.geometries : [], // not a copy for topic
- assets: isProject(topicOrProject) ? topicOrProject.assets : [], // not a copy for topic
- views: topicOrProject.views.map(view => ({
- id: crypto.randomUUID(),
- title: translated(view.title),
- permalink: view.permalink
- })),
- owner: {
- email: this.userEmail,
- name: this.userEmail.split('@')[0],
- surname: '',
- },
- editors: [],
- viewers: [],
- };
+ toCreateProject(topicOrProject: Topic | Project): CreateProject {
+ const title = isProject(topicOrProject) ?
+ `${i18next.t('tbx_copy_of_label')} ${topicOrProject.title}` :
+ translated(topicOrProject.title);
+ let description: string | undefined;
+ if (isProject(topicOrProject)) {
+ description = topicOrProject.description;
+ } else if (topicOrProject.description) {
+ description = translated(topicOrProject.description);
}
+ return {
+ title,
+ description,
+ color: isProject(topicOrProject) ? topicOrProject.color : DEFAULT_PROJECT_COLOR,
+ geometries: isProject(topicOrProject) ? topicOrProject.geometries : [], // not a copy for topic
+ assets: isProject(topicOrProject) ? topicOrProject.assets : [], // not a copy for topic
+ views: topicOrProject.views.map(view => ({
+ id: crypto.randomUUID(),
+ title: translated(view.title),
+ permalink: view.permalink
+ })),
+ owner: {
+ email: this.userEmail,
+ name: this.userEmail.split('@')[0],
+ surname: '',
+ },
+ editors: [],
+ viewers: [],
+ };
+ }
- async saveViewToProject() {
- const project: Project | undefined = isProject(this.topicOrProject) ? this.topicOrProject : undefined;
- const editorEmails = project?.editors.map(e => e.email) || [];
- if (!project || ![project.owner.email, ...editorEmails].includes(this.userEmail)) return;
- const view: View = {
- id: crypto.randomUUID(),
- title: `${i18next.t('view')} ${project?.views.length + 1}`,
- permalink: getPermalink(),
- };
- if (typeof this.selectedViewIndx !== 'number') {
- project.views.push(view);
- const success = await apiClient.updateProject(project);
- if (success) {
- DashboardStore.setViewIndex(project?.views.length - 1);
- }
- } else {
- project.views.splice(this.selectedViewIndx + 1, 0, view);
- const success = await apiClient.updateProject(project);
- if (success) {
- DashboardStore.setViewIndex(this.selectedViewIndx + 1);
- }
- }
+ async saveViewToProject() {
+ const project: Project | undefined = isProject(this.topicOrProject) ? this.topicOrProject : undefined;
+ const editorEmails = project?.editors.map(e => e.email) || [];
+ if (!project || ![project.owner.email, ...editorEmails].includes(this.userEmail)) return;
+ const view: View = {
+ id: crypto.randomUUID(),
+ title: `${i18next.t('view')} ${project?.views.length + 1}`,
+ permalink: getPermalink(),
+ };
+ if (typeof this.selectedViewIndx !== 'number') {
+ project.views.push(view);
+ const success = await this.apiClient.updateProject(project);
+ if (success) {
+ DashboardStore.setViewIndex(project?.views.length - 1);
+ }
+ } else {
+ project.views.splice(this.selectedViewIndx + 1, 0, view);
+ const success = await this.apiClient.updateProject(project);
+ if (success) {
+ DashboardStore.setViewIndex(this.selectedViewIndx + 1);
+ }
}
+ }
- createRenderRoot() {
- return this;
- }
-}
\ No newline at end of file
+ createRenderRoot() {
+ return this;
+ }
+}
diff --git a/ui/src/elements/ngm-auth.ts b/ui/src/elements/ngm-auth.ts
index d509d83e3..d5726ac93 100644
--- a/ui/src/elements/ngm-auth.ts
+++ b/ui/src/elements/ngm-auth.ts
@@ -1,72 +1,76 @@
-import {html} from 'lit';
-import type {AuthUser} from '../auth';
-import Auth from '../auth';
-import {LitElementI18n} from '../i18n.js';
-import auth from '../store/auth';
-import {classMap} from 'lit/directives/class-map.js';
-import {customElement, property, state} from 'lit/decorators.js';
-import DashboardStore from '../store/dashboard';
-
-
-/**
- * Authentication component
- */
-@customElement('ngm-auth')
-export class NgmAuth extends LitElementI18n {
- @property({type: String})
- accessor endpoint: string | undefined;
- @property({type: String})
- accessor clientId: string | undefined;
- @state()
- accessor user: AuthUser | null = null;
- private popup: Window | null = null;
-
- constructor() {
- super();
- auth.user.subscribe(user => {
- this.user = user;
- if (this.popup) {
- this.popup.close();
- this.popup = null;
- }
- });
- }
-
- async login() {
- // open the authentication popup
- const url = `${this.endpoint}?`
- + 'response_type=token'
- + `&client_id=${this.clientId}`
- + `&redirect_uri=${location.origin}${location.pathname}`
- + '&scope=openid+profile'
- + `&state=${Auth.state()}`;
-
- // open the authentication popup
- this.popup = window.open(url);
- // wait for the user to be authenticated
- await Auth.waitForAuthenticate();
- Auth.initialize();
- window.location.reload();
- }
-
- logout() {
- if (DashboardStore.projectMode.value === 'edit') {
- DashboardStore.showSaveOrCancelWarning(true);
- return;
- }
- Auth.logout();
- }
-
- render() {
- return html`
-
`;
- }
-
- createRenderRoot() {
- // no shadow dom
- return this;
- }
-}
+import {html} from 'lit';
+import type {AuthUser} from '../authService';
+import AuthService from '../authService';
+import {LitElementI18n} from '../i18n.js';
+import auth from '../store/auth';
+import {classMap} from 'lit/directives/class-map.js';
+import {customElement, property, state} from 'lit/decorators.js';
+import DashboardStore from '../store/dashboard';
+import {consume} from '@lit/context';
+import {authServiceContext} from '../context';
+
+/**
+ * Authentication component
+ */
+@customElement('ngm-auth')
+export class NgmAuth extends LitElementI18n {
+ @property({type: String})
+ accessor endpoint: string | undefined;
+ @property({type: String})
+ accessor clientId: string | undefined;
+ @state()
+ accessor user: AuthUser | null = null;
+ private popup: Window | null = null;
+
+ @consume({context: authServiceContext})
+ accessor authService!: AuthService;
+
+ constructor() {
+ super();
+ auth.user.subscribe(user => {
+ this.user = user;
+ if (this.popup) {
+ this.popup.close();
+ this.popup = null;
+ }
+ });
+ }
+
+ async login() {
+ // open the authentication popup
+ const url = `${this.endpoint}?`
+ + 'response_type=token'
+ + `&client_id=${this.clientId}`
+ + `&redirect_uri=${location.origin}${location.pathname}`
+ + '&scope=openid+profile'
+ + `&state=${this.authService.state()}`;
+
+ // open the authentication popup
+ this.popup = window.open(url);
+ // wait for the user to be authenticated
+ await this.authService.waitForAuthenticate();
+ this.authService.initialize();
+ window.location.reload();
+ }
+
+ logout() {
+ if (DashboardStore.projectMode.value === 'edit') {
+ DashboardStore.showSaveOrCancelWarning(true);
+ return;
+ }
+ this.authService.logout();
+ }
+
+ render() {
+ return html`
+
`;
+ }
+
+ createRenderRoot() {
+ // no shadow dom
+ return this;
+ }
+}
diff --git a/ui/src/elements/ngm-side-bar.ts b/ui/src/elements/ngm-side-bar.ts
index 7a53fc648..bcfb5b0f3 100644
--- a/ui/src/elements/ngm-side-bar.ts
+++ b/ui/src/elements/ngm-side-bar.ts
@@ -1,747 +1,747 @@
-import {html} from 'lit';
-import {LitElementI18n} from '../i18n.js';
-import '../toolbox/ngm-toolbox';
-import '../layers/ngm-layers';
-import '../layers/ngm-layers-sort';
-import '../layers/ngm-catalog';
-import './dashboard/ngm-dashboard';
-import LayersActions from '../layers/LayersActions';
-import {DEFAULT_LAYER_OPACITY, LayerType} from '../constants';
-import defaultLayerTree, {LayerConfig} from '../layertree';
-import {
- addAssetId,
- getAssetIds,
- getAttribute,
- getCesiumToolbarParam,
- getLayerParams,
- getSliceParam,
- getZoomToPosition,
- setCesiumToolbarParam,
- syncLayersParam
-} from '../permalink';
-import {createCesiumObject} from '../layers/helpers';
-import i18next from 'i18next';
-import 'fomantic-ui-css/components/accordion.js';
-import './ngm-map-configuration';
-import type {Cartesian2, Viewer} from 'cesium';
-import {
- BoundingSphere,
- Cartesian3,
- CustomDataSource,
- GeoJsonDataSource,
- HeadingPitchRange,
- Math as CMath,
- ScreenSpaceEventHandler,
- ScreenSpaceEventType,
-} from 'cesium';
-import {showSnackbarError, showSnackbarInfo} from '../notifications';
-import auth from '../store/auth';
-import './ngm-share-link';
-import '../layers/ngm-layers-upload';
-import MainStore from '../store/main';
-import {classMap} from 'lit/directives/class-map.js';
-import $ from '../jquery';
-import {customElement, property, query, state} from 'lit/decorators.js';
-import type QueryManager from '../query/QueryManager';
-
-import DashboardStore from '../store/dashboard';
-import {getAssets} from '../api-ion';
-import {parseKml, renderWithDelay} from '../cesiumutils';
-
-type SearchLayer = {
- layer: string
- label: string
- type?: LayerType
- title?: string
- dataSourceName?: string
-}
-
-@customElement('ngm-side-bar')
-export class SideBar extends LitElementI18n {
- @property({type: Object})
- accessor queryManager: QueryManager | null = null;
- @property({type: Boolean})
- accessor mobileView = false;
- @property({type: Boolean})
- accessor displayUndergroundHint = true;
- @state()
- accessor catalogLayers: LayerConfig[] | undefined;
- @state()
- accessor activeLayers: LayerConfig[] = [];
- @state()
- accessor activePanel: string | null = null;
- @state()
- accessor showHeader = false;
- @state()
- accessor globeQueueLength_ = 0;
- @state()
- accessor mobileShowAll = false;
- @state()
- accessor hideDataDisplayed = false;
- @state()
- accessor layerOrderChangeActive = false;
- @state()
- accessor debugToolsActive = getCesiumToolbarParam();
- @query('.ngm-side-bar-panel > .ngm-toast-placeholder')
- accessor toastPlaceholder;
- @query('ngm-catalog')
- accessor catalogElement;
- private viewer: Viewer | null = null;
- private layerActions: LayersActions | undefined;
- private zoomedToPosition = false;
- private accordionInited = false;
- private shareListenerAdded = false;
- private shareDownListener = evt => {
- if (!evt.composedPath().includes(this)) this.activePanel = null;
- };
-
- constructor() {
- super();
- MainStore.viewer.subscribe(viewer => this.viewer = viewer);
-
- auth.user.subscribe((user) => {
- if (!user && this.activeLayers) {
- // user logged out, remove restricted layers.
- const restricted = this.activeLayers.filter(config => config.restricted?.length);
- restricted.forEach(config => {
- const idx = this.activeLayers.indexOf(config);
- this.activeLayers.splice(idx, 1);
- this.removeLayer(config);
- });
- }
- });
- MainStore.setUrlLayersSubject.subscribe(async () => {
- if (this.activeLayers) {
- this.activeLayers.forEach(layer => this.removeLayerWithoutSync(layer));
- }
- await this.syncActiveLayers();
- this.catalogElement.requestUpdate();
- MainStore.nextLayersRemove();
- });
-
- MainStore.syncLayerParams.subscribe(() => {
- syncLayersParam(this.activeLayers);
- });
-
- MainStore.onIonAssetAdd.subscribe(asset => {
- const assetIds = getAssetIds();
- if (!asset.id || assetIds.includes(asset.id.toString())) {
- showSnackbarInfo(i18next.t('dtd_asset_exists_info'));
- return;
- }
- const token = MainStore.ionToken.value;
- if (!token) return;
- const layer: LayerConfig = {
- type: LayerType.tiles3d,
- assetId: asset.id,
- ionToken: token,
- label: asset.name,
- layer: asset.id.toString(),
- visible: true,
- displayed: true,
- opacityDisabled: true,
- pickable: true,
- customAsset: true,
- };
- layer.load = () => this.addLayer(layer);
- this.activeLayers.push(layer);
-
- addAssetId(asset.id);
- this.activeLayers = [...this.activeLayers];
- syncLayersParam(this.activeLayers);
- });
-
- MainStore.onRemoveIonAssets.subscribe(async () => {
- const assets = this.activeLayers.filter(l => !!l.assetId);
- for (const asset of assets) {
- await this.removeLayerWithoutSync(asset);
- }
- this.viewer!.scene.requestRender();
- this.requestUpdate();
- syncLayersParam(this.activeLayers);
- });
-
- const sliceOptions = getSliceParam();
- if (sliceOptions && sliceOptions.type && sliceOptions.slicePoints)
- this.activePanel = 'tools';
- }
-
- render() {
- if (!this.queryManager) {
- return '';
- }
-
- this.queryManager.activeLayers = this.activeLayers
- .filter(config => config.visible && !config.noQuery);
-
- const shareBtn = html`
-
this.togglePanel('share')}>
-
-
`;
- const settingsBtn = html`
-
this.togglePanel('settings')}>
-
-
`;
- const dataMobileHeader = html`
-
this.hideDataDisplayed = true}
- class="ngm-data-catalog-label ${classMap({active: this.hideDataDisplayed})}">
- ${i18next.t('lyr_geocatalog_label')}
-
-
this.hideDataDisplayed = false}
- class="ngm-data-catalog-label ${classMap({active: !this.hideDataDisplayed})}">
- ${i18next.t('dtd_displayed_data_label')}
-
`;
-
- return html`
-
-
-
this.activePanel = ''}
- @layerclick=${evt => this.onCatalogLayerClicked(evt.detail.layer)}
- >
-
-
-
this.hideDataDisplayed = !this.hideDataDisplayed}>
- ${i18next.t('dtd_configure_data_btn')}
-
-
this.onCatalogLayerClicked(evt.detail.layer)}>
-
-
-
- this.activePanel = 'tools'}
- @close=${() => this.activePanel = ''}>
-
-
-
- ${this.activePanel !== 'share' ? '' : html`
- `}
-
-
-
-
-
-
-
- ${this.layerOrderChangeActive ? i18next.t('dtd_finish_ordering_label') : i18next.t('dtd_change_order_label')}
-
- ${this.layerOrderChangeActive ?
- html`
-
this.onLayersOrderChange(evt.detail)}>
- ` :
- html`
-
this.onRemoveDisplayedLayer(evt)}
- @layerChanged=${evt => this.onLayerChanged(evt)}>
- `
- }
-
-
this.onKmlUpload(file, clampToGround)}>
-
-
-
-
-
-
-
- `;
- }
-
- togglePanel(panelName, showHeader = true) {
- if (DashboardStore.projectMode.value === 'edit') {
- DashboardStore.showSaveOrCancelWarning(true);
- return;
- }
- this.showHeader = showHeader;
- if (this.activePanel === panelName) {
- this.activePanel = null;
- return;
- }
- this.activePanel = panelName;
- if (this.activePanel === 'data' && !this.mobileView) this.hideDataDisplayed = false;
- }
-
- async syncActiveLayers() {
- const attributeParams = getAttribute();
- const callback = attributeParams ?
- this.getTileLoadCallback(attributeParams.attributeKey, attributeParams.attributeValue) :
- undefined;
- const flatLayers = this.getFlatLayers(this.catalogLayers, callback);
- const urlLayers = getLayerParams();
- const assetIds = getAssetIds();
- const ionToken = MainStore.ionToken.value;
-
- if (!urlLayers.length && !assetIds.length) {
- this.activeLayers = flatLayers.filter(l => l.displayed);
- syncLayersParam(this.activeLayers);
- return;
- }
-
- // First - make everything hidden
- flatLayers.forEach(l => {
- l.visible = false;
- l.displayed = false;
- });
-
- const activeLayers: LayerConfig[] = [];
- for (const urlLayer of urlLayers) {
- let layer = flatLayers.find(fl => fl.layer === urlLayer.layer);
- if (!layer) {
- // Layers from the search are not present in the flat layers.
- layer = this.createSearchLayer({layer: urlLayer.layer, label: urlLayer.layer}); // the proper label will be taken from getCapabilities
- } else {
- await (layer.promise || this.addLayer(layer));
- layer.add && layer.add();
- }
- layer.visible = urlLayer.visible;
- layer.opacity = urlLayer.opacity;
- layer.wmtsCurrentTime = urlLayer.timestamp || layer.wmtsCurrentTime;
- layer.setOpacity && layer.setOpacity(layer.opacity);
- layer.displayed = true;
- layer.setVisibility && layer.setVisibility(layer.visible);
- activeLayers.push(layer);
- }
-
- if (ionToken) {
- const ionAssetsRes = await getAssets(ionToken);
- const ionAssets = ionAssetsRes?.items || [];
-
- assetIds.forEach(assetId => {
- const ionAsset = ionAssets.find(asset => asset.id === Number(assetId));
- const layer: LayerConfig = {
- type: LayerType.tiles3d,
- assetId: Number(assetId),
- ionToken: ionToken,
- label: ionAsset?.name || assetId,
- layer: assetId,
- visible: true,
- displayed: true,
- opacityDisabled: true,
- pickable: true,
- customAsset: true
- };
- layer.load = () => this.addLayer(layer);
- activeLayers.push(layer);
- });
- }
-
- this.activeLayers = activeLayers;
- syncLayersParam(this.activeLayers);
- }
-
- getTileLoadCallback(attributeKey, attributeValue) {
- return (tile, removeTileLoadListener) => {
- const content = tile.content;
- const featuresLength = content.featuresLength;
- for (let i = 0; i < featuresLength; i++) {
- const feature = content.getFeature(i);
- if (feature.getProperty(attributeKey) === attributeValue) {
- removeTileLoadListener();
- this.queryManager!.selectTile(feature);
- return;
- }
- }
- };
- }
-
- async update(changedProperties) {
- if (this.viewer && !this.layerActions) {
- this.layerActions = new LayersActions(this.viewer);
- if (!this.catalogLayers) {
- this.catalogLayers = [...defaultLayerTree];
- await this.syncActiveLayers();
- }
- this.viewer.scene.globe.tileLoadProgressEvent.addEventListener(queueLength => {
- this.globeQueueLength_ = queueLength;
- });
- }
- // hide share panel on any action outside side bar
- if (!this.shareListenerAdded && this.activePanel === 'share') {
- document.addEventListener('pointerdown', this.shareDownListener);
- document.addEventListener('keydown', this.shareDownListener);
- this.shareListenerAdded = true;
- } else if (this.shareListenerAdded) {
- this.shareListenerAdded = false;
- document.removeEventListener('pointerdown', this.shareDownListener);
- document.removeEventListener('keydown', this.shareDownListener);
- }
- super.update(changedProperties);
- }
-
- updated(changedProperties) {
- if (this.queryManager) {
- !this.zoomedToPosition && this.zoomToPermalinkObject();
-
- if (!this.accordionInited && this.activePanel === 'data') {
- const panelElement = this.querySelector('.ngm-layer-catalog');
-
- if (panelElement) {
- for (let i = 0; i < panelElement.childElementCount; i++) {
- const element = panelElement.children.item(i);
- if (element && element.classList.contains('accordion')) {
- $(element).accordion({duration: 150});
- }
- }
- this.accordionInited = true;
- }
- }
- if (changedProperties.has('activeLayers')) {
- this.layerActions!.reorderLayers(this.activeLayers);
- }
- }
-
- super.updated(changedProperties);
- }
-
- async onCatalogLayerClicked(layer) {
- // toggle whether the layer is displayed or not (=listed in the side bar)
- if (layer.displayed) {
- if (layer.visible) {
- layer.displayed = false;
- layer.visible = false;
- layer.remove && layer.remove();
- const idx = this.activeLayers.findIndex(l => l.label === layer.label);
- this.activeLayers.splice(idx, 1);
- } else {
- layer.visible = true;
- }
- } else {
- await (layer.promise || this.addLayer(layer));
- layer.add && layer.add();
- layer.visible = true;
- layer.displayed = true;
- this.activeLayers.push(layer);
- this.maybeShowVisibilityHint(layer);
- }
- layer.setVisibility && layer.setVisibility(layer.visible);
-
- syncLayersParam(this.activeLayers);
- const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
- this.catalogLayers = [...catalogLayers];
- this.activeLayers = [...this.activeLayers];
- this.viewer!.scene.requestRender();
- }
-
- onLayerChanged(evt) {
- this.queryManager!.hideObjectInformation();
- const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
- this.catalogLayers = [...catalogLayers];
- this.activeLayers = [...this.activeLayers];
- syncLayersParam(this.activeLayers);
- if (evt.detail) {
- this.maybeShowVisibilityHint(evt.detail);
- }
- this.requestUpdate();
- }
-
- maybeShowVisibilityHint(config: LayerConfig) {
- if (this.displayUndergroundHint
- && config.visible
- && [LayerType.tiles3d, LayerType.earthquakes].includes(config.type!)
- && !this.viewer?.scene.cameraUnderground) {
- showSnackbarInfo(i18next.t('lyr_subsurface_hint'), {displayTime: 20000});
- this.displayUndergroundHint = false;
- }
- }
-
- async onRemoveDisplayedLayer(evt) {
- const {config, idx} = evt.detail;
- this.activeLayers.splice(idx, 1);
- await this.removeLayer(config);
- }
-
- async removeLayerWithoutSync(config: LayerConfig) {
- if (config.setVisibility) {
- config.setVisibility(false);
- } else {
- const c = await config.promise;
- if (c instanceof CustomDataSource || c instanceof GeoJsonDataSource) {
- this.viewer!.dataSources.getByName(c.name)[0].show = false;
- }
- }
- config.visible = false;
- config.displayed = false;
- if (config.remove) {
- config.remove();
- }
- }
-
- async removeLayer(config: LayerConfig) {
- await this.removeLayerWithoutSync(config);
- this.viewer!.scene.requestRender();
- syncLayersParam(this.activeLayers);
- const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
- this.catalogLayers = [...catalogLayers];
- this.activeLayers = [...this.activeLayers];
- this.requestUpdate();
- }
-
- getFlatLayers(tree, tileLoadCallback): any[] {
- const flat: any[] = [];
- for (const layer of tree) {
- if (layer.children) {
- flat.push(...this.getFlatLayers(layer.children, tileLoadCallback));
- } else {
- layer.load = () => this.addLayer(layer);
- flat.push(layer);
- }
- }
- return flat;
- }
-
- // adds layer from search to 'Displayed Layers'
- async addLayerFromSearch(searchLayer: SearchLayer) {
- let layer;
- if (searchLayer.dataSourceName) {
- layer = this.activeLayers.find(l => l.type === searchLayer.dataSourceName); // check for layers like earthquakes
- } else {
- layer = this.activeLayers.find(l => l.layer === searchLayer.layer); // check for swisstopoWMTS layers
- }
-
- if (layer) { // for layers added before
- if (layer.type === LayerType.swisstopoWMTS) {
- const index = this.activeLayers.indexOf(layer);
- this.activeLayers.splice(index, 1);
- layer.remove();
- layer.add(0);
- this.activeLayers.push(layer);
- }
- layer.setVisibility(true);
- layer.visible = true;
- layer.displayed = true;
- this.viewer!.scene.requestRender();
- } else { // for new layers
- this.activeLayers.push(this.createSearchLayer(searchLayer));
- }
- this.activeLayers = [...this.activeLayers];
- syncLayersParam(this.activeLayers);
- this.requestUpdate();
- }
-
- createSearchLayer(searchLayer: SearchLayer) {
- let config: LayerConfig;
- if (searchLayer.type) {
- config = searchLayer;
- config.visible = true;
- config.origin = 'layer';
- config.label = searchLayer.title || searchLayer.label;
- config.legend = config.type === LayerType.swisstopoWMTS ? config.layer : undefined;
- } else {
- config = {
- type: LayerType.swisstopoWMTS,
- label: searchLayer.title || searchLayer.label,
- layer: searchLayer.layer,
- visible: true,
- displayed: true,
- opacity: DEFAULT_LAYER_OPACITY,
- queryType: 'geoadmin',
- legend: searchLayer.layer
- };
- }
- config.load = async () => {
- const layer = await this.addLayer(config);
- this.activeLayers = [...this.activeLayers];
- syncLayersParam(this.activeLayers);
- return layer;
- };
-
- return config;
- }
-
- zoomToPermalinkObject() {
- this.zoomedToPosition = true;
- const zoomToPosition = getZoomToPosition();
- if (zoomToPosition) {
- let altitude = 0, cartesianPosition: Cartesian3 | undefined, windowPosition: Cartesian2 | undefined;
- const updateValues = () => {
- altitude = this.viewer!.scene.globe.getHeight(this.viewer!.scene.camera.positionCartographic) || 0;
- cartesianPosition = Cartesian3.fromDegrees(zoomToPosition.longitude, zoomToPosition.latitude, zoomToPosition.height + altitude);
- windowPosition = this.viewer!.scene.cartesianToCanvasCoordinates(cartesianPosition);
- };
- updateValues();
- const completeCallback = () => {
- if (windowPosition) {
- let maxTries = 25;
- let triesCounter = 0;
- const eventHandler = new ScreenSpaceEventHandler(this.viewer!.canvas);
- eventHandler.setInputAction(() => maxTries = 0, ScreenSpaceEventType.LEFT_DOWN);
- // Waits while will be possible to select an object
- const tryToSelect = () => setTimeout(() => {
- updateValues();
- this.zoomToObjectCoordinates(cartesianPosition);
- windowPosition && this.queryManager!.pickObject(windowPosition);
- triesCounter += 1;
- if (!this.queryManager!.objectSelector.selectedObj && triesCounter <= maxTries) {
- tryToSelect();
- } else {
- eventHandler.destroy();
- if (triesCounter > maxTries) {
- showSnackbarError(i18next.t('dtd_object_on_coordinates_not_found_warning'));
- }
- }
- }, 500);
- tryToSelect();
- }
-
- };
- this.zoomToObjectCoordinates(cartesianPosition, completeCallback);
- }
- }
-
- zoomToObjectCoordinates(center, complete?) {
- const boundingSphere = new BoundingSphere(center, 1000);
- const zoomHeadingPitchRange = new HeadingPitchRange(
- 0,
- -CMath.toRadians(45),
- boundingSphere.radius);
- this.viewer!.scene.camera.flyToBoundingSphere(boundingSphere, {
- duration: 0,
- offset: zoomHeadingPitchRange,
- complete: complete
- });
- }
-
- addLayer(layer: LayerConfig) {
- layer.promise = createCesiumObject(this.viewer!, layer);
- this.dispatchEvent(new CustomEvent('layeradded', {
- detail: {
- layer
- }
- }));
- return layer.promise;
- }
-
- toggleLayerOrderChange() {
- this.layerOrderChangeActive = !this.layerOrderChangeActive;
- }
-
- async onLayersOrderChange(layers: LayerConfig[]) {
- await this.layerActions!.reorderLayers(layers);
- // update activeLayers only when ordering finished
- if (!this.layerOrderChangeActive) {
- this.activeLayers = [...layers];
- }
- this.dispatchEvent(new CustomEvent('layerChanged'));
- }
-
- async onKmlUpload(file: File, clampToGround: boolean) {
- if (!this.viewer) return;
- const dataSource = new CustomDataSource();
- const name = await parseKml(this.viewer, file, dataSource, clampToGround);
- const layer = `${name.replace(' ', '_')}_${Date.now()}`;
- // name used as id for datasource
- dataSource.name = layer;
- MainStore.addUploadedKmlName(dataSource.name);
- await this.viewer.dataSources.add(dataSource);
- await renderWithDelay(this.viewer);
- // done like this to have correct rerender of component
- const promise = Promise.resolve(dataSource);
- const config: LayerConfig = {
- load() { return promise; },
- label: name,
- layer,
- promise: promise,
- opacity: DEFAULT_LAYER_OPACITY,
- notSaveToPermalink: true,
- ownKml: true,
- opacityDisabled: true
- };
- await this.onCatalogLayerClicked(config);
- this.viewer.zoomTo(dataSource);
- this.requestUpdate();
- }
-
- toggleDebugTools(event) {
- const active = event.target.checked;
- this.debugToolsActive = active;
- setCesiumToolbarParam(active);
- this.dispatchEvent(new CustomEvent('toggleDebugTools', {detail: {active}}));
- }
-
- createRenderRoot() {
- return this;
- }
-}
+import {html} from 'lit';
+import {LitElementI18n} from '../i18n.js';
+import '../toolbox/ngm-toolbox';
+import '../layers/ngm-layers';
+import '../layers/ngm-layers-sort';
+import '../layers/ngm-catalog';
+import './dashboard/ngm-dashboard';
+import LayersActions from '../layers/LayersActions';
+import {DEFAULT_LAYER_OPACITY, LayerType} from '../constants';
+import defaultLayerTree, {LayerConfig} from '../layertree';
+import {
+ addAssetId,
+ getAssetIds,
+ getAttribute,
+ getCesiumToolbarParam,
+ getLayerParams,
+ getSliceParam,
+ getZoomToPosition,
+ setCesiumToolbarParam,
+ syncLayersParam
+} from '../permalink';
+import {createCesiumObject} from '../layers/helpers';
+import i18next from 'i18next';
+import 'fomantic-ui-css/components/accordion.js';
+import './ngm-map-configuration';
+import type {Cartesian2, Viewer} from 'cesium';
+import {
+ BoundingSphere,
+ Cartesian3,
+ CustomDataSource,
+ GeoJsonDataSource,
+ HeadingPitchRange,
+ Math as CMath,
+ ScreenSpaceEventHandler,
+ ScreenSpaceEventType,
+} from 'cesium';
+import {showSnackbarError, showSnackbarInfo} from '../notifications';
+import auth from '../store/auth';
+import './ngm-share-link';
+import '../layers/ngm-layers-upload';
+import MainStore from '../store/main';
+import {classMap} from 'lit/directives/class-map.js';
+import $ from '../jquery';
+import {customElement, property, query, state} from 'lit/decorators.js';
+import type QueryManager from '../query/QueryManager';
+
+import DashboardStore from '../store/dashboard';
+import {getAssets} from '../api-ion';
+import {parseKml, renderWithDelay} from '../cesiumutils';
+
+type SearchLayer = {
+ layer: string
+ label: string
+ type?: LayerType
+ title?: string
+ dataSourceName?: string
+}
+
+@customElement('ngm-side-bar')
+export class SideBar extends LitElementI18n {
+ @property({type: Object})
+ accessor queryManager: QueryManager | null = null;
+ @property({type: Boolean})
+ accessor mobileView = false;
+ @property({type: Boolean})
+ accessor displayUndergroundHint = true;
+ @state()
+ accessor catalogLayers: LayerConfig[] | undefined;
+ @state()
+ accessor activeLayers: LayerConfig[] = [];
+ @state()
+ accessor activePanel: string | null = null;
+ @state()
+ accessor showHeader = false;
+ @state()
+ accessor globeQueueLength_ = 0;
+ @state()
+ accessor mobileShowAll = false;
+ @state()
+ accessor hideDataDisplayed = false;
+ @state()
+ accessor layerOrderChangeActive = false;
+ @state()
+ accessor debugToolsActive = getCesiumToolbarParam();
+ @query('.ngm-side-bar-panel > .ngm-toast-placeholder')
+ accessor toastPlaceholder;
+ @query('ngm-catalog')
+ accessor catalogElement;
+ private viewer: Viewer | null = null;
+ private layerActions: LayersActions | undefined;
+ private zoomedToPosition = false;
+ private accordionInited = false;
+ private shareListenerAdded = false;
+ private shareDownListener = evt => {
+ if (!evt.composedPath().includes(this)) this.activePanel = null;
+ };
+
+ constructor() {
+ super();
+ MainStore.viewer.subscribe(viewer => this.viewer = viewer);
+
+ auth.user.subscribe((user) => {
+ if (!user && this.activeLayers) {
+ // user logged out, remove restricted layers.
+ const restricted = this.activeLayers.filter(config => config.restricted?.length);
+ restricted.forEach(config => {
+ const idx = this.activeLayers.indexOf(config);
+ this.activeLayers.splice(idx, 1);
+ this.removeLayer(config);
+ });
+ }
+ });
+ MainStore.setUrlLayersSubject.subscribe(async () => {
+ if (this.activeLayers) {
+ this.activeLayers.forEach(layer => this.removeLayerWithoutSync(layer));
+ }
+ await this.syncActiveLayers();
+ this.catalogElement.requestUpdate();
+ MainStore.nextLayersRemove();
+ });
+
+ MainStore.syncLayerParams.subscribe(() => {
+ syncLayersParam(this.activeLayers);
+ });
+
+ MainStore.onIonAssetAdd.subscribe(asset => {
+ const assetIds = getAssetIds();
+ if (!asset.id || assetIds.includes(asset.id.toString())) {
+ showSnackbarInfo(i18next.t('dtd_asset_exists_info'));
+ return;
+ }
+ const token = MainStore.ionToken.value;
+ if (!token) return;
+ const layer: LayerConfig = {
+ type: LayerType.tiles3d,
+ assetId: asset.id,
+ ionToken: token,
+ label: asset.name,
+ layer: asset.id.toString(),
+ visible: true,
+ displayed: true,
+ opacityDisabled: true,
+ pickable: true,
+ customAsset: true,
+ };
+ layer.load = () => this.addLayer(layer);
+ this.activeLayers.push(layer);
+
+ addAssetId(asset.id);
+ this.activeLayers = [...this.activeLayers];
+ syncLayersParam(this.activeLayers);
+ });
+
+ MainStore.onRemoveIonAssets.subscribe(async () => {
+ const assets = this.activeLayers.filter(l => !!l.assetId);
+ for (const asset of assets) {
+ await this.removeLayerWithoutSync(asset);
+ }
+ this.viewer!.scene.requestRender();
+ this.requestUpdate();
+ syncLayersParam(this.activeLayers);
+ });
+
+ const sliceOptions = getSliceParam();
+ if (sliceOptions && sliceOptions.type && sliceOptions.slicePoints)
+ this.activePanel = 'tools';
+ }
+
+ render() {
+ if (!this.queryManager) {
+ return '';
+ }
+
+ this.queryManager.activeLayers = this.activeLayers
+ .filter(config => config.visible && !config.noQuery);
+
+ const shareBtn = html`
+
this.togglePanel('share')}>
+
+
`;
+ const settingsBtn = html`
+
this.togglePanel('settings')}>
+
+
`;
+ const dataMobileHeader = html`
+
this.hideDataDisplayed = true}
+ class="ngm-data-catalog-label ${classMap({active: this.hideDataDisplayed})}">
+ ${i18next.t('lyr_geocatalog_label')}
+
+
this.hideDataDisplayed = false}
+ class="ngm-data-catalog-label ${classMap({active: !this.hideDataDisplayed})}">
+ ${i18next.t('dtd_displayed_data_label')}
+
`;
+
+ return html`
+
+
+
this.activePanel = ''}
+ @layerclick=${evt => this.onCatalogLayerClicked(evt.detail.layer)}
+ >
+
+
+
this.hideDataDisplayed = !this.hideDataDisplayed}>
+ ${i18next.t('dtd_configure_data_btn')}
+
+
this.onCatalogLayerClicked(evt.detail.layer)}>
+
+
+
+ this.activePanel = 'tools'}
+ @close=${() => this.activePanel = ''}>
+
+
+
+ ${this.activePanel !== 'share' ? '' : html`
+ `}
+
+
+
+
+
+
+
+ ${this.layerOrderChangeActive ? i18next.t('dtd_finish_ordering_label') : i18next.t('dtd_change_order_label')}
+
+ ${this.layerOrderChangeActive ?
+ html`
+
this.onLayersOrderChange(evt.detail)}>
+ ` :
+ html`
+
this.onRemoveDisplayedLayer(evt)}
+ @layerChanged=${evt => this.onLayerChanged(evt)}>
+ `
+ }
+
+
this.onKmlUpload(file, clampToGround)}>
+
+
+
+
+
+
+
+ `;
+ }
+
+ togglePanel(panelName, showHeader = true) {
+ if (DashboardStore.projectMode.value === 'edit') {
+ DashboardStore.showSaveOrCancelWarning(true);
+ return;
+ }
+ this.showHeader = showHeader;
+ if (this.activePanel === panelName) {
+ this.activePanel = null;
+ return;
+ }
+ this.activePanel = panelName;
+ if (this.activePanel === 'data' && !this.mobileView) this.hideDataDisplayed = false;
+ }
+
+ async syncActiveLayers() {
+ const attributeParams = getAttribute();
+ const callback = attributeParams ?
+ this.getTileLoadCallback(attributeParams.attributeKey, attributeParams.attributeValue) :
+ undefined;
+ const flatLayers = this.getFlatLayers(this.catalogLayers, callback);
+ const urlLayers = getLayerParams();
+ const assetIds = getAssetIds();
+ const ionToken = MainStore.ionToken.value;
+
+ if (!urlLayers.length && !assetIds.length) {
+ this.activeLayers = flatLayers.filter(l => l.displayed);
+ syncLayersParam(this.activeLayers);
+ return;
+ }
+
+ // First - make everything hidden
+ flatLayers.forEach(l => {
+ l.visible = false;
+ l.displayed = false;
+ });
+
+ const activeLayers: LayerConfig[] = [];
+ for (const urlLayer of urlLayers) {
+ let layer = flatLayers.find(fl => fl.layer === urlLayer.layer);
+ if (!layer) {
+ // Layers from the search are not present in the flat layers.
+ layer = this.createSearchLayer({layer: urlLayer.layer, label: urlLayer.layer}); // the proper label will be taken from getCapabilities
+ } else {
+ await (layer.promise || this.addLayer(layer));
+ layer.add && layer.add();
+ }
+ layer.visible = urlLayer.visible;
+ layer.opacity = urlLayer.opacity;
+ layer.wmtsCurrentTime = urlLayer.timestamp || layer.wmtsCurrentTime;
+ layer.setOpacity && layer.setOpacity(layer.opacity);
+ layer.displayed = true;
+ layer.setVisibility && layer.setVisibility(layer.visible);
+ activeLayers.push(layer);
+ }
+
+ if (ionToken) {
+ const ionAssetsRes = await getAssets(ionToken);
+ const ionAssets = ionAssetsRes?.items || [];
+
+ assetIds.forEach(assetId => {
+ const ionAsset = ionAssets.find(asset => asset.id === Number(assetId));
+ const layer: LayerConfig = {
+ type: LayerType.tiles3d,
+ assetId: Number(assetId),
+ ionToken: ionToken,
+ label: ionAsset?.name || assetId,
+ layer: assetId,
+ visible: true,
+ displayed: true,
+ opacityDisabled: true,
+ pickable: true,
+ customAsset: true
+ };
+ layer.load = () => this.addLayer(layer);
+ activeLayers.push(layer);
+ });
+ }
+
+ this.activeLayers = activeLayers;
+ syncLayersParam(this.activeLayers);
+ }
+
+ getTileLoadCallback(attributeKey, attributeValue) {
+ return (tile, removeTileLoadListener) => {
+ const content = tile.content;
+ const featuresLength = content.featuresLength;
+ for (let i = 0; i < featuresLength; i++) {
+ const feature = content.getFeature(i);
+ if (feature.getProperty(attributeKey) === attributeValue) {
+ removeTileLoadListener();
+ this.queryManager!.selectTile(feature);
+ return;
+ }
+ }
+ };
+ }
+
+ async update(changedProperties) {
+ if (this.viewer && !this.layerActions) {
+ this.layerActions = new LayersActions(this.viewer);
+ if (!this.catalogLayers) {
+ this.catalogLayers = [...defaultLayerTree];
+ await this.syncActiveLayers();
+ }
+ this.viewer.scene.globe.tileLoadProgressEvent.addEventListener(queueLength => {
+ this.globeQueueLength_ = queueLength;
+ });
+ }
+ // hide share panel on any action outside side bar
+ if (!this.shareListenerAdded && this.activePanel === 'share') {
+ document.addEventListener('pointerdown', this.shareDownListener);
+ document.addEventListener('keydown', this.shareDownListener);
+ this.shareListenerAdded = true;
+ } else if (this.shareListenerAdded) {
+ this.shareListenerAdded = false;
+ document.removeEventListener('pointerdown', this.shareDownListener);
+ document.removeEventListener('keydown', this.shareDownListener);
+ }
+ super.update(changedProperties);
+ }
+
+ updated(changedProperties) {
+ if (this.queryManager) {
+ !this.zoomedToPosition && this.zoomToPermalinkObject();
+
+ if (!this.accordionInited && this.activePanel === 'data') {
+ const panelElement = this.querySelector('.ngm-layer-catalog');
+
+ if (panelElement) {
+ for (let i = 0; i < panelElement.childElementCount; i++) {
+ const element = panelElement.children.item(i);
+ if (element && element.classList.contains('accordion')) {
+ $(element).accordion({duration: 150});
+ }
+ }
+ this.accordionInited = true;
+ }
+ }
+ if (changedProperties.has('activeLayers')) {
+ this.layerActions!.reorderLayers(this.activeLayers);
+ }
+ }
+
+ super.updated(changedProperties);
+ }
+
+ async onCatalogLayerClicked(layer) {
+ // toggle whether the layer is displayed or not (=listed in the side bar)
+ if (layer.displayed) {
+ if (layer.visible) {
+ layer.displayed = false;
+ layer.visible = false;
+ layer.remove && layer.remove();
+ const idx = this.activeLayers.findIndex(l => l.label === layer.label);
+ this.activeLayers.splice(idx, 1);
+ } else {
+ layer.visible = true;
+ }
+ } else {
+ await (layer.promise || this.addLayer(layer));
+ layer.add && layer.add();
+ layer.visible = true;
+ layer.displayed = true;
+ this.activeLayers.push(layer);
+ this.maybeShowVisibilityHint(layer);
+ }
+ layer.setVisibility && layer.setVisibility(layer.visible);
+
+ syncLayersParam(this.activeLayers);
+ const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
+ this.catalogLayers = [...catalogLayers];
+ this.activeLayers = [...this.activeLayers];
+ this.viewer!.scene.requestRender();
+ }
+
+ onLayerChanged(evt) {
+ this.queryManager!.hideObjectInformation();
+ const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
+ this.catalogLayers = [...catalogLayers];
+ this.activeLayers = [...this.activeLayers];
+ syncLayersParam(this.activeLayers);
+ if (evt.detail) {
+ this.maybeShowVisibilityHint(evt.detail);
+ }
+ this.requestUpdate();
+ }
+
+ maybeShowVisibilityHint(config: LayerConfig) {
+ if (this.displayUndergroundHint
+ && config.visible
+ && [LayerType.tiles3d, LayerType.earthquakes].includes(config.type!)
+ && !this.viewer?.scene.cameraUnderground) {
+ showSnackbarInfo(i18next.t('lyr_subsurface_hint'), {displayTime: 20000});
+ this.displayUndergroundHint = false;
+ }
+ }
+
+ async onRemoveDisplayedLayer(evt) {
+ const {config, idx} = evt.detail;
+ this.activeLayers.splice(idx, 1);
+ await this.removeLayer(config);
+ }
+
+ async removeLayerWithoutSync(config: LayerConfig) {
+ if (config.setVisibility) {
+ config.setVisibility(false);
+ } else {
+ const c = await config.promise;
+ if (c instanceof CustomDataSource || c instanceof GeoJsonDataSource) {
+ this.viewer!.dataSources.getByName(c.name)[0].show = false;
+ }
+ }
+ config.visible = false;
+ config.displayed = false;
+ if (config.remove) {
+ config.remove();
+ }
+ }
+
+ async removeLayer(config: LayerConfig) {
+ await this.removeLayerWithoutSync(config);
+ this.viewer!.scene.requestRender();
+ syncLayersParam(this.activeLayers);
+ const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
+ this.catalogLayers = [...catalogLayers];
+ this.activeLayers = [...this.activeLayers];
+ this.requestUpdate();
+ }
+
+ getFlatLayers(tree, tileLoadCallback): any[] {
+ const flat: any[] = [];
+ for (const layer of tree) {
+ if (layer.children) {
+ flat.push(...this.getFlatLayers(layer.children, tileLoadCallback));
+ } else {
+ layer.load = () => this.addLayer(layer);
+ flat.push(layer);
+ }
+ }
+ return flat;
+ }
+
+ // adds layer from search to 'Displayed Layers'
+ async addLayerFromSearch(searchLayer: SearchLayer) {
+ let layer;
+ if (searchLayer.dataSourceName) {
+ layer = this.activeLayers.find(l => l.type === searchLayer.dataSourceName); // check for layers like earthquakes
+ } else {
+ layer = this.activeLayers.find(l => l.layer === searchLayer.layer); // check for swisstopoWMTS layers
+ }
+
+ if (layer) { // for layers added before
+ if (layer.type === LayerType.swisstopoWMTS) {
+ const index = this.activeLayers.indexOf(layer);
+ this.activeLayers.splice(index, 1);
+ layer.remove();
+ layer.add(0);
+ this.activeLayers.push(layer);
+ }
+ layer.setVisibility(true);
+ layer.visible = true;
+ layer.displayed = true;
+ this.viewer!.scene.requestRender();
+ } else { // for new layers
+ this.activeLayers.push(this.createSearchLayer(searchLayer));
+ }
+ this.activeLayers = [...this.activeLayers];
+ syncLayersParam(this.activeLayers);
+ this.requestUpdate();
+ }
+
+ createSearchLayer(searchLayer: SearchLayer) {
+ let config: LayerConfig;
+ if (searchLayer.type) {
+ config = searchLayer;
+ config.visible = true;
+ config.origin = 'layer';
+ config.label = searchLayer.title || searchLayer.label;
+ config.legend = config.type === LayerType.swisstopoWMTS ? config.layer : undefined;
+ } else {
+ config = {
+ type: LayerType.swisstopoWMTS,
+ label: searchLayer.title || searchLayer.label,
+ layer: searchLayer.layer,
+ visible: true,
+ displayed: true,
+ opacity: DEFAULT_LAYER_OPACITY,
+ queryType: 'geoadmin',
+ legend: searchLayer.layer
+ };
+ }
+ config.load = async () => {
+ const layer = await this.addLayer(config);
+ this.activeLayers = [...this.activeLayers];
+ syncLayersParam(this.activeLayers);
+ return layer;
+ };
+
+ return config;
+ }
+
+ zoomToPermalinkObject() {
+ this.zoomedToPosition = true;
+ const zoomToPosition = getZoomToPosition();
+ if (zoomToPosition) {
+ let altitude = 0, cartesianPosition: Cartesian3 | undefined, windowPosition: Cartesian2 | undefined;
+ const updateValues = () => {
+ altitude = this.viewer!.scene.globe.getHeight(this.viewer!.scene.camera.positionCartographic) || 0;
+ cartesianPosition = Cartesian3.fromDegrees(zoomToPosition.longitude, zoomToPosition.latitude, zoomToPosition.height + altitude);
+ windowPosition = this.viewer!.scene.cartesianToCanvasCoordinates(cartesianPosition);
+ };
+ updateValues();
+ const completeCallback = () => {
+ if (windowPosition) {
+ let maxTries = 25;
+ let triesCounter = 0;
+ const eventHandler = new ScreenSpaceEventHandler(this.viewer!.canvas);
+ eventHandler.setInputAction(() => maxTries = 0, ScreenSpaceEventType.LEFT_DOWN);
+ // Waits while will be possible to select an object
+ const tryToSelect = () => setTimeout(() => {
+ updateValues();
+ this.zoomToObjectCoordinates(cartesianPosition);
+ windowPosition && this.queryManager!.pickObject(windowPosition);
+ triesCounter += 1;
+ if (!this.queryManager!.objectSelector.selectedObj && triesCounter <= maxTries) {
+ tryToSelect();
+ } else {
+ eventHandler.destroy();
+ if (triesCounter > maxTries) {
+ showSnackbarError(i18next.t('dtd_object_on_coordinates_not_found_warning'));
+ }
+ }
+ }, 500);
+ tryToSelect();
+ }
+
+ };
+ this.zoomToObjectCoordinates(cartesianPosition, completeCallback);
+ }
+ }
+
+ zoomToObjectCoordinates(center, complete?) {
+ const boundingSphere = new BoundingSphere(center, 1000);
+ const zoomHeadingPitchRange = new HeadingPitchRange(
+ 0,
+ -CMath.toRadians(45),
+ boundingSphere.radius);
+ this.viewer!.scene.camera.flyToBoundingSphere(boundingSphere, {
+ duration: 0,
+ offset: zoomHeadingPitchRange,
+ complete: complete
+ });
+ }
+
+ addLayer(layer: LayerConfig) {
+ layer.promise = createCesiumObject(this.viewer!, layer);
+ this.dispatchEvent(new CustomEvent('layeradded', {
+ detail: {
+ layer
+ }
+ }));
+ return layer.promise;
+ }
+
+ toggleLayerOrderChange() {
+ this.layerOrderChangeActive = !this.layerOrderChangeActive;
+ }
+
+ async onLayersOrderChange(layers: LayerConfig[]) {
+ await this.layerActions!.reorderLayers(layers);
+ // update activeLayers only when ordering finished
+ if (!this.layerOrderChangeActive) {
+ this.activeLayers = [...layers];
+ }
+ this.dispatchEvent(new CustomEvent('layerChanged'));
+ }
+
+ async onKmlUpload(file: File, clampToGround: boolean) {
+ if (!this.viewer) return;
+ const dataSource = new CustomDataSource();
+ const name = await parseKml(this.viewer, file, dataSource, clampToGround);
+ const layer = `${name.replace(' ', '_')}_${Date.now()}`;
+ // name used as id for datasource
+ dataSource.name = layer;
+ MainStore.addUploadedKmlName(dataSource.name);
+ await this.viewer.dataSources.add(dataSource);
+ await renderWithDelay(this.viewer);
+ // done like this to have correct rerender of component
+ const promise = Promise.resolve(dataSource);
+ const config: LayerConfig = {
+ load() { return promise; },
+ label: name,
+ layer,
+ promise: promise,
+ opacity: DEFAULT_LAYER_OPACITY,
+ notSaveToPermalink: true,
+ ownKml: true,
+ opacityDisabled: true
+ };
+ await this.onCatalogLayerClicked(config);
+ this.viewer.zoomTo(dataSource);
+ this.requestUpdate();
+ }
+
+ toggleDebugTools(event) {
+ const active = event.target.checked;
+ this.debugToolsActive = active;
+ setCesiumToolbarParam(active);
+ this.dispatchEvent(new CustomEvent('toggleDebugTools', {detail: {active}}));
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+}
diff --git a/ui/src/index.ts b/ui/src/index.ts
index bcfd23bd7..a8857f652 100644
--- a/ui/src/index.ts
+++ b/ui/src/index.ts
@@ -1,11 +1,7 @@
-import './style/index.css';
-import {ReactiveElement} from 'lit';
-
-// Detect issues following lit2 migration
-ReactiveElement.enableWarning?.('migration');
-
-import Auth from './auth';
-
-import './ngm-app';
-
-Auth.initialize();
+import './style/index.css';
+import {ReactiveElement} from 'lit';
+
+// Detect issues following lit2 migration
+ReactiveElement.enableWarning?.('migration');
+
+import './ngm-app-boot';
diff --git a/ui/src/layers/helpers.ts b/ui/src/layers/helpers.ts
index 617547267..b8583d493 100644
--- a/ui/src/layers/helpers.ts
+++ b/ui/src/layers/helpers.ts
@@ -1,259 +1,259 @@
-import EarthquakeVisualizer from '../earthquakeVisualization/earthquakeVisualizer.js';
-import {ImageryLayer, Rectangle, Viewer} from 'cesium';
-import {
- Cartesian3,
- Cartographic,
- Cesium3DTileColorBlendMode,
- Cesium3DTileset,
- Cesium3DTileStyle,
- Cesium3DTilesVoxelProvider,
- Ellipsoid,
- GeoJsonDataSource,
- IonResource,
- LabelStyle,
- Matrix3,
- Matrix4,
- VoxelPrimitive,
-} from 'cesium';
-import {getSwisstopoImagery} from '../swisstopoImagery';
-import {LayerType} from '../constants';
-import {isLabelOutlineEnabled} from '../permalink';
-import AmazonS3Resource from '../AmazonS3Resource.js';
-import {getVoxelShader} from './voxels-helper';
-import MainStore from '../store/main';
-import {LayerConfig} from '../layertree';
-
-export interface PickableCesium3DTileset extends Cesium3DTileset {
- pickable?: boolean;
-}
-export interface PickableVoxelPrimitive extends VoxelPrimitive {
- pickable?: boolean;
- layer?: string;
-}
-
-export async function createEarthquakeFromConfig(viewer: Viewer, config: LayerConfig) {
- const earthquakeVisualizer = new EarthquakeVisualizer(viewer, config);
- if (config.visible) {
- await earthquakeVisualizer.setVisible(true);
- }
- config.setVisibility = visible => earthquakeVisualizer.setVisible(visible);
- config.setOpacity = (opacity: number) => earthquakeVisualizer.setOpacity(opacity);
- return earthquakeVisualizer;
-}
-
-export function createIonGeoJSONFromConfig(viewer: Viewer, config) {
- return IonResource.fromAssetId(config.assetId)
- .then(resource => GeoJsonDataSource.load(resource))
- .then(dataSource => {
- viewer.dataSources.add(dataSource);
- dataSource.show = !!config.visible;
- config.setVisibility = visible => dataSource.show = !!visible;
- return dataSource;
- });
-}
-
-
-export async function create3DVoxelsTilesetFromConfig(viewer: Viewer, config: LayerConfig, _): Promise
{
- const provider = await Cesium3DTilesVoxelProvider.fromUrl(config.url!);
-
- const primitive: PickableVoxelPrimitive = new VoxelPrimitive({
- provider: provider,
- });
-
- const searchParams = new URLSearchParams(location.search);
- const stepSize = parseFloat(searchParams.get('stepSize') || '1');
-
- primitive.nearestSampling = true;
- primitive.stepSize = stepSize;
- primitive.depthTest = true;
- primitive.show = !!config.visible;
- primitive.pickable = config.pickable !== undefined ? config.pickable : false;
- primitive.layer = config.layer;
-
- viewer.scene.primitives.add(primitive);
-
- config.setVisibility = visible => {
- if (config.type === LayerType.voxels3dtiles) {
- if (visible) MainStore.addVisibleVoxelLayer(config.layer);
- else MainStore.removeVisibleVoxelLayer(config.layer);
- }
- primitive.show = !!visible;
- };
-
- if (config.voxelDataName && !primitive.provider.names.includes(config.voxelDataName)) {
- throw new Error(`Voxel data name ${config.voxelDataName} not found in the tileset`);
- }
- primitive.customShader = getVoxelShader(config);
- primitive.jitter = false;
-
- return primitive;
-}
-export async function create3DTilesetFromConfig(viewer: Viewer, config: LayerConfig, tileLoadCallback) {
- let resource: string | IonResource | AmazonS3Resource;
- if (config.aws_s3_bucket && config.aws_s3_key) {
- resource = new AmazonS3Resource({
- bucket: config.aws_s3_bucket,
- url: config.aws_s3_key,
- });
- } else if (config.url) {
- resource = config.url;
- } else {
- resource = await IonResource.fromAssetId(config.assetId!, {
- accessToken: config.ionToken,
- });
- }
-
- const tileset: PickableCesium3DTileset = await Cesium3DTileset.fromUrl(resource, {
- show: !!config.visible,
- backFaceCulling: false,
- maximumScreenSpaceError: tileLoadCallback ? Number.NEGATIVE_INFINITY : 16, // 16 - default value
- });
-
- if (config.style) {
- if (config.layer === 'ch.swisstopo.swissnames3d.3d') { // for performance testing
- config.style.labelStyle = isLabelOutlineEnabled() ? LabelStyle.FILL_AND_OUTLINE : LabelStyle.FILL;
- }
- tileset.style = new Cesium3DTileStyle(config.style);
- }
-
- tileset.pickable = config.pickable !== undefined ? config.pickable : false;
- viewer.scene.primitives.add(tileset);
-
- config.setVisibility = visible => {
- tileset.show = !!visible;
- };
-
- if (!config.opacityDisabled) {
- config.setOpacity = opacity => {
- const style = config.style;
- if (style && (style.color || style.labelColor)) {
- const {propertyName, colorType, colorValue} = styleColorParser(style);
- const color = `${colorType}(${colorValue}, ${opacity})`;
- tileset.style = new Cesium3DTileStyle({...style, [propertyName]: color});
- } else {
- const color = `color("white", ${opacity})`;
- tileset.style = new Cesium3DTileStyle({...style, color});
- }
- };
- config.setOpacity(config.opacity ? config.opacity : 1);
- }
-
- if (tileLoadCallback) {
- const removeTileLoadListener = tileset.tileLoad.addEventListener(tile => tileLoadCallback(tile, removeTileLoadListener));
- }
-
- if (config.propsOrder) {
- tileset.properties.propsOrder = config.propsOrder;
- }
- if (config.heightOffset) {
- const cartographic = Cartographic.fromCartesian(tileset.boundingSphere.center);
- const surface = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
- const offset = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, config.heightOffset);
- const translation = Cartesian3.subtract(offset, surface, new Cartesian3());
- tileset.modelMatrix = Matrix4.fromTranslation(translation);
- viewer.scene.requestRender();
- }
- // for correct highlighting
- tileset.colorBlendMode = Cesium3DTileColorBlendMode.REPLACE;
- return tileset;
-}
-
-export async function createSwisstopoWMTSImageryLayer(viewer: Viewer, config: LayerConfig) {
- const layer: ImageryLayer = await getSwisstopoImagery(config);
- config.setVisibility = visible => layer.show = !!visible;
- config.setOpacity = opacity => layer.alpha = opacity;
- config.remove = () => viewer.scene.imageryLayers.remove(layer, false);
- config.add = (toIndex) => {
- const layersLength = viewer.scene.imageryLayers.length;
- if (toIndex > 0 && toIndex < layersLength) {
- const imageryIndex = layersLength - toIndex;
- viewer.scene.imageryLayers.add(layer, imageryIndex);
- return;
- }
- viewer.scene.imageryLayers.add(layer);
- };
- config.setTime = (time: string) => {
- config.wmtsCurrentTime = time;
- layer.show = false;
- viewer.scene.render();
- setTimeout(() => {
- layer.show = true;
- viewer.scene.render();
- }, 100);
- };
- viewer.scene.imageryLayers.add(layer);
- layer.alpha = config.opacity || 1;
- layer.show = !!config.visible;
- return layer;
-}
-
-
-export function createCesiumObject(viewer: Viewer, config: LayerConfig, tileLoadCallback?) {
- const factories = {
- [LayerType.ionGeoJSON]: createIonGeoJSONFromConfig,
- [LayerType.tiles3d]: create3DTilesetFromConfig,
- [LayerType.voxels3dtiles]: create3DVoxelsTilesetFromConfig,
- [LayerType.swisstopoWMTS]: createSwisstopoWMTSImageryLayer,
- [LayerType.earthquakes]: createEarthquakeFromConfig,
- };
- return factories[config.type!](viewer, config, tileLoadCallback);
-}
-
-function styleColorParser(style: any) {
- const propertyName = style.color ? 'color' : 'labelColor';
- let colorType = style[propertyName].slice(0, style[propertyName].indexOf('('));
- const lastIndex = colorType === 'rgba' ? style[propertyName].lastIndexOf(',') : style[propertyName].indexOf(')');
- const colorValue = style[propertyName].slice(style[propertyName].indexOf('(') + 1, lastIndex);
- colorType = colorType === 'rgb' ? 'rgba' : colorType;
- return {propertyName, colorType, colorValue};
-}
-
-
-export function getBoxFromRectangle(rectangle: Rectangle, height: number, result: Cartesian3 = new Cartesian3()): Cartesian3 {
- const sw = Cartographic.toCartesian(Rectangle.southwest(rectangle, new Cartographic()));
- const se = Cartographic.toCartesian(Rectangle.southeast(rectangle, new Cartographic()));
- const nw = Cartographic.toCartesian(Rectangle.northwest(rectangle, new Cartographic()));
- result.x = Cartesian3.distance(sw, se); // gets box width
- result.y = Cartesian3.distance(sw, nw); // gets box length
- result.z = height;
- return result;
-}
-
-/**
- * Returns rectangle from width height and center point
- */
-export function calculateRectangle(width: number, height: number, center: Cartesian3, result: Rectangle = new Rectangle()): Rectangle {
- const w = new Cartesian3(center.x, center.y - width / 2, center.z);
- result.west = Ellipsoid.WGS84.cartesianToCartographic(w).longitude;
- const s = new Cartesian3(center.x + height / 2, center.y, center.z);
- result.south = Ellipsoid.WGS84.cartesianToCartographic(s).latitude;
- const e = new Cartesian3(center.x, center.y + width / 2, center.z);
- result.east = Ellipsoid.WGS84.cartesianToCartographic(e).longitude;
- const n = new Cartesian3(center.x - height / 2, center.y, center.z);
- result.north = Ellipsoid.WGS84.cartesianToCartographic(n).latitude;
-
- return result;
-}
-
-/**
- * Calculates box from bounding volume
- */
-export function calculateBox(halfAxes: Matrix3, boundingSphereRadius: number, result: Cartesian3 = new Cartesian3()): Cartesian3 {
- const absMatrix = Matrix3.abs(halfAxes, new Matrix3());
- for (let i = 0; i < 3; i++) {
- const column = Matrix3.getColumn(absMatrix, i, new Cartesian3());
- const row = Matrix3.getRow(absMatrix, i, new Cartesian3());
- result.y = result.y + column.x + row.x;
- result.x = result.x + column.y + row.y;
- result.z = result.z + column.z + row.z;
- }
- // scale according to bounding sphere
- const diagonal = Math.sqrt(result.x * result.x + result.y * result.y);
- const radius = boundingSphereRadius;
- const scale = Math.max(diagonal / (radius * 2), (radius * 2) / diagonal);
- result.x = result.x * scale;
- result.y = result.y * scale;
- result.z = result.z > 60000 ? 60000 : result.z;
-
- return new Cartesian3(result.x, result.y, result.z);
-}
+import EarthquakeVisualizer from '../earthquakeVisualization/earthquakeVisualizer.js';
+import {ImageryLayer, Rectangle, Viewer} from 'cesium';
+import {
+ Cartesian3,
+ Cartographic,
+ Cesium3DTileColorBlendMode,
+ Cesium3DTileset,
+ Cesium3DTileStyle,
+ Cesium3DTilesVoxelProvider,
+ Ellipsoid,
+ GeoJsonDataSource,
+ IonResource,
+ LabelStyle,
+ Matrix3,
+ Matrix4,
+ VoxelPrimitive,
+} from 'cesium';
+import {getSwisstopoImagery} from '../swisstopoImagery';
+import {LayerType} from '../constants';
+import {isLabelOutlineEnabled} from '../permalink';
+import AmazonS3Resource from '../AmazonS3Resource.js';
+import {getVoxelShader} from './voxels-helper';
+import MainStore from '../store/main';
+import {LayerConfig} from '../layertree';
+
+export interface PickableCesium3DTileset extends Cesium3DTileset {
+ pickable?: boolean;
+}
+export interface PickableVoxelPrimitive extends VoxelPrimitive {
+ pickable?: boolean;
+ layer?: string;
+}
+
+export async function createEarthquakeFromConfig(viewer: Viewer, config: LayerConfig) {
+ const earthquakeVisualizer = new EarthquakeVisualizer(viewer, config);
+ if (config.visible) {
+ await earthquakeVisualizer.setVisible(true);
+ }
+ config.setVisibility = visible => earthquakeVisualizer.setVisible(visible);
+ config.setOpacity = (opacity: number) => earthquakeVisualizer.setOpacity(opacity);
+ return earthquakeVisualizer;
+}
+
+export function createIonGeoJSONFromConfig(viewer: Viewer, config) {
+ return IonResource.fromAssetId(config.assetId)
+ .then(resource => GeoJsonDataSource.load(resource))
+ .then(dataSource => {
+ viewer.dataSources.add(dataSource);
+ dataSource.show = !!config.visible;
+ config.setVisibility = visible => dataSource.show = !!visible;
+ return dataSource;
+ });
+}
+
+
+export async function create3DVoxelsTilesetFromConfig(viewer: Viewer, config: LayerConfig, _): Promise {
+ const provider = await Cesium3DTilesVoxelProvider.fromUrl(config.url!);
+
+ const primitive: PickableVoxelPrimitive = new VoxelPrimitive({
+ provider: provider,
+ });
+
+ const searchParams = new URLSearchParams(location.search);
+ const stepSize = parseFloat(searchParams.get('stepSize') || '1');
+
+ primitive.nearestSampling = true;
+ primitive.stepSize = stepSize;
+ primitive.depthTest = true;
+ primitive.show = !!config.visible;
+ primitive.pickable = config.pickable !== undefined ? config.pickable : false;
+ primitive.layer = config.layer;
+
+ viewer.scene.primitives.add(primitive);
+
+ config.setVisibility = visible => {
+ if (config.type === LayerType.voxels3dtiles) {
+ if (visible) MainStore.addVisibleVoxelLayer(config.layer);
+ else MainStore.removeVisibleVoxelLayer(config.layer);
+ }
+ primitive.show = !!visible;
+ };
+
+ if (config.voxelDataName && !primitive.provider.names.includes(config.voxelDataName)) {
+ throw new Error(`Voxel data name ${config.voxelDataName} not found in the tileset`);
+ }
+ primitive.customShader = getVoxelShader(config);
+ primitive.jitter = false;
+
+ return primitive;
+}
+export async function create3DTilesetFromConfig(viewer: Viewer, config: LayerConfig, tileLoadCallback) {
+ let resource: string | IonResource | AmazonS3Resource;
+ if (config.aws_s3_bucket && config.aws_s3_key) {
+ resource = new AmazonS3Resource({
+ bucket: config.aws_s3_bucket,
+ url: config.aws_s3_key,
+ });
+ } else if (config.url) {
+ resource = config.url;
+ } else {
+ resource = await IonResource.fromAssetId(config.assetId!, {
+ accessToken: config.ionToken,
+ });
+ }
+
+ const tileset: PickableCesium3DTileset = await Cesium3DTileset.fromUrl(resource, {
+ show: !!config.visible,
+ backFaceCulling: false,
+ maximumScreenSpaceError: tileLoadCallback ? Number.NEGATIVE_INFINITY : 16, // 16 - default value
+ });
+
+ if (config.style) {
+ if (config.layer === 'ch.swisstopo.swissnames3d.3d') { // for performance testing
+ config.style.labelStyle = isLabelOutlineEnabled() ? LabelStyle.FILL_AND_OUTLINE : LabelStyle.FILL;
+ }
+ tileset.style = new Cesium3DTileStyle(config.style);
+ }
+
+ tileset.pickable = config.pickable !== undefined ? config.pickable : false;
+ viewer.scene.primitives.add(tileset);
+
+ config.setVisibility = visible => {
+ tileset.show = !!visible;
+ };
+
+ if (!config.opacityDisabled) {
+ config.setOpacity = opacity => {
+ const style = config.style;
+ if (style && (style.color || style.labelColor)) {
+ const {propertyName, colorType, colorValue} = styleColorParser(style);
+ const color = `${colorType}(${colorValue}, ${opacity})`;
+ tileset.style = new Cesium3DTileStyle({...style, [propertyName]: color});
+ } else {
+ const color = `color("white", ${opacity})`;
+ tileset.style = new Cesium3DTileStyle({...style, color});
+ }
+ };
+ config.setOpacity(config.opacity ? config.opacity : 1);
+ }
+
+ if (tileLoadCallback) {
+ const removeTileLoadListener = tileset.tileLoad.addEventListener(tile => tileLoadCallback(tile, removeTileLoadListener));
+ }
+
+ if (config.propsOrder) {
+ tileset.properties.propsOrder = config.propsOrder;
+ }
+ if (config.heightOffset) {
+ const cartographic = Cartographic.fromCartesian(tileset.boundingSphere.center);
+ const surface = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0);
+ const offset = Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, config.heightOffset);
+ const translation = Cartesian3.subtract(offset, surface, new Cartesian3());
+ tileset.modelMatrix = Matrix4.fromTranslation(translation);
+ viewer.scene.requestRender();
+ }
+ // for correct highlighting
+ tileset.colorBlendMode = Cesium3DTileColorBlendMode.REPLACE;
+ return tileset;
+}
+
+export async function createSwisstopoWMTSImageryLayer(viewer: Viewer, config: LayerConfig) {
+ const layer: ImageryLayer = await getSwisstopoImagery(config);
+ config.setVisibility = visible => layer.show = !!visible;
+ config.setOpacity = opacity => layer.alpha = opacity;
+ config.remove = () => viewer.scene.imageryLayers.remove(layer, false);
+ config.add = (toIndex) => {
+ const layersLength = viewer.scene.imageryLayers.length;
+ if (toIndex > 0 && toIndex < layersLength) {
+ const imageryIndex = layersLength - toIndex;
+ viewer.scene.imageryLayers.add(layer, imageryIndex);
+ return;
+ }
+ viewer.scene.imageryLayers.add(layer);
+ };
+ config.setTime = (time: string) => {
+ config.wmtsCurrentTime = time;
+ layer.show = false;
+ viewer.scene.render();
+ setTimeout(() => {
+ layer.show = true;
+ viewer.scene.render();
+ }, 100);
+ };
+ viewer.scene.imageryLayers.add(layer);
+ layer.alpha = config.opacity || 1;
+ layer.show = !!config.visible;
+ return layer;
+}
+
+
+export function createCesiumObject(viewer: Viewer, config: LayerConfig, tileLoadCallback?) {
+ const factories = {
+ [LayerType.ionGeoJSON]: createIonGeoJSONFromConfig,
+ [LayerType.tiles3d]: create3DTilesetFromConfig,
+ [LayerType.voxels3dtiles]: create3DVoxelsTilesetFromConfig,
+ [LayerType.swisstopoWMTS]: createSwisstopoWMTSImageryLayer,
+ [LayerType.earthquakes]: createEarthquakeFromConfig,
+ };
+ return factories[config.type!](viewer, config, tileLoadCallback);
+}
+
+function styleColorParser(style: any) {
+ const propertyName = style.color ? 'color' : 'labelColor';
+ let colorType = style[propertyName].slice(0, style[propertyName].indexOf('('));
+ const lastIndex = colorType === 'rgba' ? style[propertyName].lastIndexOf(',') : style[propertyName].indexOf(')');
+ const colorValue = style[propertyName].slice(style[propertyName].indexOf('(') + 1, lastIndex);
+ colorType = colorType === 'rgb' ? 'rgba' : colorType;
+ return {propertyName, colorType, colorValue};
+}
+
+
+export function getBoxFromRectangle(rectangle: Rectangle, height: number, result: Cartesian3 = new Cartesian3()): Cartesian3 {
+ const sw = Cartographic.toCartesian(Rectangle.southwest(rectangle, new Cartographic()));
+ const se = Cartographic.toCartesian(Rectangle.southeast(rectangle, new Cartographic()));
+ const nw = Cartographic.toCartesian(Rectangle.northwest(rectangle, new Cartographic()));
+ result.x = Cartesian3.distance(sw, se); // gets box width
+ result.y = Cartesian3.distance(sw, nw); // gets box length
+ result.z = height;
+ return result;
+}
+
+/**
+ * Returns rectangle from width height and center point
+ */
+export function calculateRectangle(width: number, height: number, center: Cartesian3, result: Rectangle = new Rectangle()): Rectangle {
+ const w = new Cartesian3(center.x, center.y - width / 2, center.z);
+ result.west = Ellipsoid.WGS84.cartesianToCartographic(w).longitude;
+ const s = new Cartesian3(center.x + height / 2, center.y, center.z);
+ result.south = Ellipsoid.WGS84.cartesianToCartographic(s).latitude;
+ const e = new Cartesian3(center.x, center.y + width / 2, center.z);
+ result.east = Ellipsoid.WGS84.cartesianToCartographic(e).longitude;
+ const n = new Cartesian3(center.x - height / 2, center.y, center.z);
+ result.north = Ellipsoid.WGS84.cartesianToCartographic(n).latitude;
+
+ return result;
+}
+
+/**
+ * Calculates box from bounding volume
+ */
+export function calculateBox(halfAxes: Matrix3, boundingSphereRadius: number, result: Cartesian3 = new Cartesian3()): Cartesian3 {
+ const absMatrix = Matrix3.abs(halfAxes, new Matrix3());
+ for (let i = 0; i < 3; i++) {
+ const column = Matrix3.getColumn(absMatrix, i, new Cartesian3());
+ const row = Matrix3.getRow(absMatrix, i, new Cartesian3());
+ result.y = result.y + column.x + row.x;
+ result.x = result.x + column.y + row.y;
+ result.z = result.z + column.z + row.z;
+ }
+ // scale according to bounding sphere
+ const diagonal = Math.sqrt(result.x * result.x + result.y * result.y);
+ const radius = boundingSphereRadius;
+ const scale = Math.max(diagonal / (radius * 2), (radius * 2) / diagonal);
+ result.x = result.x * scale;
+ result.y = result.y * scale;
+ result.z = result.z > 60000 ? 60000 : result.z;
+
+ return new Cartesian3(result.x, result.y, result.z);
+}
diff --git a/ui/src/ngm-app-boot.ts b/ui/src/ngm-app-boot.ts
new file mode 100644
index 000000000..fa4618c9d
--- /dev/null
+++ b/ui/src/ngm-app-boot.ts
@@ -0,0 +1,39 @@
+import {LitElement, html} from 'lit';
+import {customElement} from 'lit/decorators.js';
+import './ngm-app';
+import {Task} from '@lit/task';
+
+import {ClientConfig} from './api/client-config';
+import {registerAppContext} from './context';
+import {ConfigService} from './api/config.service';
+
+
+@customElement('ngm-app-boot')
+export class NgmAppBoot extends LitElement {
+ private viewerInitialization = new Task(this, {
+ task: async () => {
+ const clientConfig = await new ConfigService().getConfig() as ClientConfig;
+ if (!clientConfig) {
+ console.error('Failed to load client config');
+ return;
+ }
+
+ registerAppContext(this, clientConfig);
+ },
+ args: () => [],
+ });
+
+ render() {
+ return this.viewerInitialization.render({
+ pending: () => html`Loading
`,
+ complete: () => html`
+ `,
+ error: (e) => html`Error: ${e}
`
+ });
+ }
+
+ // This deactivates shadow DOM. Because this is done for all other components, we have to add it for the time being.
+ createRenderRoot() {
+ return this;
+ }
+}
diff --git a/ui/src/ngm-app.ts b/ui/src/ngm-app.ts
index 374b61bb0..e1047f574 100644
--- a/ui/src/ngm-app.ts
+++ b/ui/src/ngm-app.ts
@@ -57,6 +57,9 @@ import DashboardStore from './store/dashboard';
import type {SideBar} from './elements/ngm-side-bar';
import {LayerConfig} from './layertree';
import $ from './jquery';
+import {clientConfigContext} from './context';
+import {consume} from '@lit/context';
+import {ClientConfig} from './api/client-config';
const SKIP_STEP2_TIMEOUT = 5000;
@@ -89,7 +92,7 @@ export class NgmApp extends LitElementI18n {
@state()
accessor showMobileSearch = false;
@state()
- accessor loading = true;
+ accessor loading = false;
@state()
accessor determinateLoading = false;
@state()
@@ -123,6 +126,9 @@ export class NgmApp extends LitElementI18n {
private waitForViewLoading = false;
private resolutionScaleRemoveCallback: Event.RemoveCallback | undefined;
+ @consume({context: clientConfigContext})
+ accessor clientConfig!: ClientConfig;
+
constructor() {
super();
@@ -505,7 +511,7 @@ export class NgmApp extends LitElementI18n {
${this.showTrackingConsent ? html`
` : ''}
this.showIonModal = false}>
diff --git a/ui/src/store/auth.ts b/ui/src/store/auth.ts
index 7c2efdd15..c1ba6a919 100644
--- a/ui/src/store/auth.ts
+++ b/ui/src/store/auth.ts
@@ -1,19 +1,19 @@
-import {BehaviorSubject} from 'rxjs';
-import type {AuthUser} from '../auth';
-
-export default class AuthStore {
- private static userSubject = new BehaviorSubject