diff --git a/src/apps/illumination/ngv-main-illumination.ts b/src/apps/illumination/ngv-main-illumination.ts
index 00a77c5..afee3dc 100644
--- a/src/apps/illumination/ngv-main-illumination.ts
+++ b/src/apps/illumination/ngv-main-illumination.ts
@@ -6,6 +6,7 @@ import {type CesiumWidget} from '@cesium/engine';
import type {IIlluminationConfig} from './ingv-config-illumination.js';
import '../../plugins/cesium/ngv-plugin-cesium-widget.js';
+import type {ViewerInitializedDetails} from '../../plugins/cesium/ngv-plugin-cesium-widget.js';
@customElement('ngv-main-illumination')
export class NgvMainIllumination extends LitElement {
@@ -23,6 +24,7 @@ export class NgvMainIllumination extends LitElement {
`;
updated(): void {
+ if (!this.viewer?.clock) return;
this.viewer.clock.currentTime = this.date;
console.log(this.date.toString());
}
@@ -32,8 +34,8 @@ export class NgvMainIllumination extends LitElement {
) => {
- this.viewer = evt.detail;
+ @viewerInitialized=${(evt: CustomEvent) => {
+ this.viewer = evt.detail.viewer;
this.updated();
}}
>
diff --git a/src/apps/permits/demoPermitConfig.ts b/src/apps/permits/demoPermitConfig.ts
index c9d7bc8..0829e9e 100644
--- a/src/apps/permits/demoPermitConfig.ts
+++ b/src/apps/permits/demoPermitConfig.ts
@@ -43,6 +43,9 @@ export const config: IPermitsConfig = {
widgetOptions: {
scene3DOnly: true,
},
+ globeOptions: {
+ depthTestAgainstTerrain: true,
+ },
},
},
};
diff --git a/src/apps/permits/index.ts b/src/apps/permits/index.ts
index f86c982..f925f43 100644
--- a/src/apps/permits/index.ts
+++ b/src/apps/permits/index.ts
@@ -1,6 +1,6 @@
import type {HTMLTemplateResult} from 'lit';
import {html} from 'lit';
-import {customElement} from 'lit/decorators.js';
+import {customElement, state} from 'lit/decorators.js';
import '../../structure/ngv-structure-app.js';
@@ -11,42 +11,31 @@ import {ABaseApp} from '../../structure/BaseApp.js';
import type {IPermitsConfig} from './ingv-config-permits.js';
import '../../plugins/cesium/ngv-plugin-cesium-widget';
-import type {CesiumWidget, Model} from '@cesium/engine';
+import '../../plugins/cesium/ngv-plugin-cesium-upload';
+import '../../plugins/cesium/ngv-plugin-cesium-model-interact';
+import type {CesiumWidget, DataSourceCollection} from '@cesium/engine';
-import {
- Math as CesiumMath,
- Ellipsoid,
- HeadingPitchRoll,
- Transforms,
-} from '@cesium/engine';
+import {PrimitiveCollection} from '@cesium/engine';
+import type {ViewerInitializedDetails} from '../../plugins/cesium/ngv-plugin-cesium-widget.js';
@customElement('ngv-app-permits')
@localized()
export class NgvAppPermits extends ABaseApp
{
+ @state()
private viewer: CesiumWidget;
+ private uploadedModelsCollection: PrimitiveCollection =
+ new PrimitiveCollection();
+ private dataSourceCollection: DataSourceCollection;
- constructor() {
- super(() => import('./demoPermitConfig.js'));
- }
-
- modelCallback(name: string, model: Model): void {
- // This position the model where the camera is
- console.log('positioning', name);
- const positionClone = this.viewer.camera.position.clone();
+ private storeOptions = {
+ localStoreKey: 'permits-localStoreModels',
+ indexDbName: 'permits-uploadedModelsStore',
+ };
- const fixedFrameTransform = Transforms.localFrameToFixedFrameGenerator(
- 'north',
- 'west',
- );
+ private collections: ViewerInitializedDetails['primitiveCollections'];
- const modelOrientation = [90, 0, 0];
- const modelMatrix = Transforms.headingPitchRollToFixedFrame(
- positionClone,
- new HeadingPitchRoll(...modelOrientation.map(CesiumMath.toRadians)),
- Ellipsoid.WGS84,
- fixedFrameTransform,
- );
- model.modelMatrix = modelMatrix;
+ constructor() {
+ super(() => import('./demoPermitConfig.js'));
}
render(): HTMLTemplateResult {
@@ -56,11 +45,44 @@ export class NgvAppPermits extends ABaseApp {
}
return html`
+
+ ${this.viewer
+ ? html`
+
+
+
+
+ `
+ : ''}
+
+
) => {
- this.viewer = evt.detail;
+ @viewerInitialized=${(evt: CustomEvent) => {
+ this.viewer = evt.detail.viewer;
+ this.viewer.scene.primitives.add(this.uploadedModelsCollection);
+ this.dataSourceCollection = evt.detail.dataSourceCollection;
+ this.collections = evt.detail.primitiveCollections;
}}
>
diff --git a/src/catalogs/demoCatalog.ts b/src/catalogs/demoCatalog.ts
index 9b23a64..9645de3 100644
--- a/src/catalogs/demoCatalog.ts
+++ b/src/catalogs/demoCatalog.ts
@@ -12,6 +12,9 @@ export const catalog: INGVCatalog = {
credit: 'test',
},
},
+ position: [6.628484, 46.5],
+ height: 0,
+ rotation: 0,
},
sofa: {
type: 'model',
@@ -19,6 +22,9 @@ export const catalog: INGVCatalog = {
url: 'https://raw.GithubUserContent.com/KhronosGroup/glTF-Sample-Assets/main/./Models/SheenWoodLeatherSofa/glTF-Binary/SheenWoodLeatherSofa.glb',
credit: 'Khonos',
},
+ position: [6.628484, 46.5],
+ height: 0,
+ rotation: 0,
},
// to complete
},
diff --git a/src/interfaces/cesium/ingv-cesium-context.ts b/src/interfaces/cesium/ingv-cesium-context.ts
index 22da76e..bf6089c 100644
--- a/src/interfaces/cesium/ingv-cesium-context.ts
+++ b/src/interfaces/cesium/ingv-cesium-context.ts
@@ -1,4 +1,4 @@
-import type {CesiumWidget} from '@cesium/engine';
+import type {CesiumWidget, Globe} from '@cesium/engine';
import type {INGVCatalog} from './ingv-catalog.js';
export interface IngvCesiumContext {
@@ -32,4 +32,5 @@ export interface IngvCesiumContext {
};
};
widgetOptions?: ConstructorParameters[1];
+ globeOptions?: Partial;
}
diff --git a/src/interfaces/cesium/ingv-layers.ts b/src/interfaces/cesium/ingv-layers.ts
index a80d6e2..75525a8 100644
--- a/src/interfaces/cesium/ingv-layers.ts
+++ b/src/interfaces/cesium/ingv-layers.ts
@@ -6,6 +6,7 @@ import type {
} from '@cesium/engine';
import type {CesiumTerrainProvider, Model} from '@cesium/engine';
+import type {Cartesian3} from '@cesium/engine';
export type INGVCesiumImageryTypes =
| INGVCesiumWMSImagery
@@ -18,7 +19,7 @@ export type INGVCesiumAllTypes =
| INGVCesiumImageryTypes;
export type INGVCesiumAllPrimitiveTypes =
- | INGVCesiumModel
+ | INGVCesiumModelConfig
| INGVIFC
| INGVCesium3DTiles;
@@ -29,17 +30,30 @@ export interface INGVCesium3DTiles {
options?: ConstructorParameters[0];
}
-export interface INGVCesiumModel {
+export interface INGVCesiumModelConfig {
type: 'model';
options?: Parameters[0];
+ position?: [number, number];
+ height?: number;
+ rotation?: number;
+}
+
+export interface INGVCesiumModel extends Model {
+ id: {
+ dimensions?: Cartesian3;
+ name: string;
+ };
}
export interface INGVIFC {
type: 'ifc';
url: string;
options?: {
- modelOptions: Omit;
+ modelOptions: Omit;
};
+ position: [number, number];
+ height: number;
+ rotation: number;
}
export interface INGVCesiumTerrain {
diff --git a/src/plugins/cesium/interactionHelpers.ts b/src/plugins/cesium/interactionHelpers.ts
new file mode 100644
index 0000000..5e9b82c
--- /dev/null
+++ b/src/plugins/cesium/interactionHelpers.ts
@@ -0,0 +1,507 @@
+import type {CustomDataSource, Model, Scene} from '@cesium/engine';
+import {
+ ArcType,
+ Axis,
+ BoundingSphere,
+ CallbackProperty,
+ Cartesian2,
+ Cartesian3,
+ Cartographic,
+ Color,
+ Ellipsoid,
+ HeadingPitchRoll,
+ IntersectionTests,
+ Matrix3,
+ Matrix4,
+ Plane,
+ Quaternion,
+ Ray,
+ TranslationRotationScale,
+} from '@cesium/engine';
+import type {INGVCesiumModel} from '../../interfaces/cesium/ingv-layers.js';
+
+export type PlaneColorOptions = {
+ material?: Color;
+ outline?: boolean;
+ outlineColor?: Color;
+};
+
+export type EdgeStyleOptions = {
+ width?: number;
+ material?: Color;
+};
+
+export type CornerPointStyleOptions = {
+ radiusPx?: number;
+ material?: Color;
+};
+
+export type BBoxStyles = {
+ planeColorOptions?: PlaneColorOptions;
+ edgeStyleOptions?: EdgeStyleOptions;
+ cornerPointStyleOptions: CornerPointStyleOptions;
+};
+
+const DefaultPlaneColorOptions: PlaneColorOptions = {
+ material: Color.RED.withAlpha(0.1),
+ outline: true,
+ outlineColor: Color.WHITE,
+};
+
+const DefaultEdgeStyles: EdgeStyleOptions = {
+ width: 10,
+ material: Color.WHITE.withAlpha(0.3),
+};
+
+const DefaultCornerPointStyles: CornerPointStyleOptions = {
+ radiusPx: 10,
+ material: Color.BROWN,
+};
+
+const SIDE_PLANES: Plane[] = [
+ new Plane(new Cartesian3(0, 0, 1), 0.5),
+ new Plane(new Cartesian3(0, 0, -1), 0.5),
+ new Plane(new Cartesian3(0, 1, 0), 0.5),
+ new Plane(new Cartesian3(0, -1, 0), 0.5),
+ new Plane(new Cartesian3(1, 0, 0), 0.5),
+ new Plane(new Cartesian3(-1, 0, 0), 0.5),
+];
+
+const CORNER_POINT_VECTORS = [
+ new Cartesian3(0.5, 0.5, 0.5),
+ new Cartesian3(0.5, -0.5, 0.5),
+ new Cartesian3(-0.5, -0.5, 0.5),
+ new Cartesian3(-0.5, 0.5, 0.5),
+];
+
+const LOCAL_EDGES: [Cartesian3, Cartesian3][] = [];
+CORNER_POINT_VECTORS.forEach((vector, i) => {
+ const upPoint = vector;
+ const downPoint = Cartesian3.clone(upPoint, new Cartesian3());
+ downPoint.z *= -1;
+ const nextUpPoint = CORNER_POINT_VECTORS[(i + 1) % 4];
+ const nextDownPoint = Cartesian3.clone(nextUpPoint, new Cartesian3());
+ nextDownPoint.z *= -1;
+ const verticalEdge: [Cartesian3, Cartesian3] = [downPoint, upPoint];
+ // const topEdge: [Cartesian3, Cartesian3] = [nextUpPoint, upPoint];
+ // const bottomEdge: [Cartesian3, Cartesian3] = [nextDownPoint, downPoint];
+ LOCAL_EDGES.push(verticalEdge);
+});
+
+const scaleScratch = new Cartesian3();
+export function getScaleFromMatrix(matrix: Matrix4): Cartesian3 {
+ return Matrix4.getScale(matrix, scaleScratch);
+}
+
+const dimensionsScratch = new Cartesian3();
+function getScaledDimensions(model: INGVCesiumModel): Cartesian3 {
+ Cartesian3.clone(model.id.dimensions, dimensionsScratch);
+ Cartesian3.multiplyComponents(
+ getScaleFromMatrix(model.modelMatrix),
+ dimensionsScratch,
+ dimensionsScratch,
+ );
+ return dimensionsScratch;
+}
+
+const scratchRotationMatrix = new Matrix3();
+const scratchRotationQuaternion = new Quaternion();
+export function getRotationQuaternionFromMatrix(matrix: Matrix4): Quaternion {
+ return Quaternion.fromRotationMatrix(
+ Matrix4.getRotation(matrix, scratchRotationMatrix),
+ scratchRotationQuaternion,
+ );
+}
+
+const scratchTranslation = new Cartesian3();
+export function getTranslationFromMatrix(matrix: Matrix4): Cartesian3 {
+ return Matrix4.getTranslation(matrix, scratchTranslation);
+}
+
+const scratchTranslationRotationDimensionsMatrix = new Matrix4();
+export function getTranslationRotationDimensionsMatrix(
+ model: INGVCesiumModel,
+ result = scratchTranslationRotationDimensionsMatrix,
+): Matrix4 {
+ return Matrix4.fromTranslationRotationScale(
+ new TranslationRotationScale(
+ getTranslationFromMatrix(model.modelMatrix),
+ getRotationQuaternionFromMatrix(model.modelMatrix),
+ getScaledDimensions(model),
+ ),
+ result,
+ );
+}
+
+const scratchTranslationRotationScaleMatrix = new Matrix4();
+export function getTranslationRotationScaleMatrix(
+ matrix: Matrix4,
+ result = scratchTranslationRotationScaleMatrix,
+): Matrix4 {
+ return Matrix4.fromTranslationRotationScale(
+ new TranslationRotationScale(
+ getTranslationFromMatrix(matrix),
+ getRotationQuaternionFromMatrix(matrix),
+ getScaleFromMatrix(matrix),
+ ),
+ result,
+ );
+}
+
+const centerDiffScratch = new Cartesian3();
+export function getModelCenterDiff(model: Model): Cartesian3 {
+ return Cartesian3.subtract(
+ model.boundingSphere.center,
+ getTranslationFromMatrix(model.modelMatrix),
+ centerDiffScratch,
+ );
+}
+
+const scaleMatrixScratch = new Matrix4();
+const planeScaleScratch = new Cartesian3();
+function getPlaneScale(model: INGVCesiumModel) {
+ const dimensions = getScaledDimensions(model);
+ Cartesian3.fromArray(
+ [dimensions.x, dimensions.y, dimensions.z],
+ 0,
+ planeScaleScratch,
+ );
+
+ return Matrix4.fromScale(planeScaleScratch, scaleMatrixScratch);
+}
+
+const planeDimensionsScratch = new Cartesian2();
+function getPlaneDimensions(model: INGVCesiumModel, normalAxis: Axis) {
+ const dimensions = getScaledDimensions(model);
+ let dimensionsArray: number[] = [];
+
+ if (normalAxis === Axis.X) {
+ dimensionsArray = [dimensions.y, dimensions.z];
+ } else if (normalAxis === Axis.Y) {
+ dimensionsArray = [dimensions.x, dimensions.z];
+ } else if (normalAxis === Axis.Z) {
+ dimensionsArray = [dimensions.x, dimensions.y];
+ }
+
+ return Cartesian2.fromArray(dimensionsArray, 0, planeDimensionsScratch);
+}
+
+export function createPlaneEntity(
+ dataSource: CustomDataSource,
+ plane: Plane,
+ model: INGVCesiumModel,
+ colorOptions: PlaneColorOptions = DefaultPlaneColorOptions,
+): void {
+ const normalAxis: Axis = plane.normal.x
+ ? Axis.X
+ : plane.normal.y
+ ? Axis.Y
+ : Axis.Z;
+
+ dataSource.entities.add({
+ position: new CallbackProperty(() => model.boundingSphere.center, false),
+ orientation: new CallbackProperty(
+ () => getRotationQuaternionFromMatrix(model.modelMatrix),
+ false,
+ ),
+ plane: {
+ plane: new CallbackProperty(
+ () => Plane.transform(plane, getPlaneScale(model)),
+ false,
+ ),
+ dimensions: new CallbackProperty(
+ () => getPlaneDimensions(model, normalAxis),
+ false,
+ ),
+ ...colorOptions,
+ },
+ });
+}
+export function createEdge(
+ dataSource: CustomDataSource,
+ model: INGVCesiumModel,
+ edge: Cartesian3[],
+ styles: EdgeStyleOptions = DefaultEdgeStyles,
+): void {
+ const positions = [new Cartesian3(), new Cartesian3()];
+ dataSource.entities.add({
+ polyline: {
+ show: true,
+ positions: new CallbackProperty(() => {
+ const matrix = getTranslationRotationDimensionsMatrix(model);
+ Matrix4.multiplyByPoint(matrix, edge[0], positions[0]);
+ Matrix4.multiplyByPoint(matrix, edge[1], positions[1]);
+ const centerDiff = getModelCenterDiff(model);
+ Cartesian3.add(positions[0], centerDiff, positions[0]);
+ Cartesian3.add(positions[1], centerDiff, positions[1]);
+ return positions;
+ }, false),
+ width: styles.width,
+ material: styles.material,
+ arcType: ArcType.NONE,
+ },
+ });
+}
+
+export function createCornerPoint(
+ dataSource: CustomDataSource,
+ model: INGVCesiumModel,
+ edges: Cartesian3[],
+ scene: Scene,
+ styles: CornerPointStyleOptions = DefaultCornerPointStyles,
+): void {
+ const position = new Cartesian3();
+ const boundingSphere = new BoundingSphere();
+ edges.forEach((localEdge) => {
+ dataSource.entities.add({
+ position: new CallbackProperty(() => {
+ const matrix = getTranslationRotationDimensionsMatrix(model);
+ Matrix4.multiplyByPoint(matrix, localEdge, position);
+ const centerDiff = getModelCenterDiff(model);
+ Cartesian3.add(position, centerDiff, position);
+ boundingSphere.center = position;
+ return position;
+ }, false),
+ ellipsoid: {
+ show: true,
+ radii: new CallbackProperty(() => {
+ const pixelSize = getPixelSize(scene, boundingSphere);
+ const worldRadius = pixelSize * styles.radiusPx;
+ return new Cartesian3(worldRadius, worldRadius, worldRadius);
+ }, false),
+ material: styles.material,
+ },
+ });
+ });
+}
+
+export function showModelBBox(
+ dataSources: {
+ topDownPlanesDataSource?: CustomDataSource;
+ sidePlanesDataSource?: CustomDataSource;
+ edgeLinesDataSource?: CustomDataSource;
+ cornerPointsDataSource?: CustomDataSource;
+ },
+ model: INGVCesiumModel,
+ scene: Scene,
+ styles: BBoxStyles = {
+ planeColorOptions: DefaultPlaneColorOptions,
+ edgeStyleOptions: DefaultEdgeStyles,
+ cornerPointStyleOptions: DefaultCornerPointStyles,
+ },
+): void {
+ const topDownPlanesDataSource = dataSources.topDownPlanesDataSource;
+ const sidePlanesDataSource = dataSources.sidePlanesDataSource;
+ if (topDownPlanesDataSource || sidePlanesDataSource) {
+ SIDE_PLANES.forEach((plane) => {
+ const normalAxis: Axis = plane.normal.x
+ ? Axis.X
+ : plane.normal.y
+ ? Axis.Y
+ : Axis.Z;
+
+ const dataSource =
+ normalAxis === Axis.Z ? topDownPlanesDataSource : sidePlanesDataSource;
+ if (dataSource)
+ createPlaneEntity(dataSource, plane, model, styles.planeColorOptions);
+ });
+ }
+ if (dataSources.edgeLinesDataSource) {
+ LOCAL_EDGES.forEach((edge) =>
+ createEdge(
+ dataSources.edgeLinesDataSource,
+ model,
+ edge,
+ styles.edgeStyleOptions,
+ ),
+ );
+ }
+ if (dataSources.cornerPointsDataSource) {
+ LOCAL_EDGES.forEach((edge) =>
+ createCornerPoint(
+ dataSources.cornerPointsDataSource,
+ model,
+ edge,
+ scene,
+ styles.cornerPointStyleOptions,
+ ),
+ );
+ }
+}
+
+const scratchQuaternionRotate = new Quaternion();
+const scratchMatrix3Rotate = new Matrix3();
+const scratchHpr = new HeadingPitchRoll();
+export function rotate(
+ startPosition: Cartesian2,
+ endPosition: Cartesian2,
+ matrixToRotate: Matrix4,
+): void {
+ const dx = endPosition.x - startPosition.x;
+ const sensitivity = 0.5;
+ const heading = -dx * sensitivity;
+ HeadingPitchRoll.fromDegrees(heading, 0, 0, scratchHpr);
+
+ Matrix3.fromQuaternion(
+ Quaternion.fromHeadingPitchRoll(scratchHpr, scratchQuaternionRotate),
+ scratchMatrix3Rotate,
+ );
+
+ Matrix4.multiplyByMatrix3(
+ matrixToRotate,
+ scratchMatrix3Rotate,
+ matrixToRotate,
+ );
+}
+
+const scratchScale = new Cartesian3();
+const scratchScaleMatrix = new Matrix4();
+// todo use axis to have correct scale direction
+export function scale(
+ startPosition: Cartesian2,
+ endPosition: Cartesian2,
+ matrix: Matrix4,
+): void {
+ const dx = endPosition.x - startPosition.x;
+ const sensitivity = 0.01;
+ const scaleAmount = 1 + dx * sensitivity;
+
+ Matrix4.fromScale(
+ Cartesian3.fromArray(
+ [scaleAmount, scaleAmount, scaleAmount],
+ 0,
+ scratchScale,
+ ),
+ scratchScaleMatrix,
+ );
+ Matrix4.multiply(matrix, scratchScaleMatrix, matrix);
+}
+
+const scratchPickedPositionCart = new Cartographic();
+const scratchPickedPosition = new Cartesian3();
+const scratchUpDirection = new Cartesian3();
+const scratchTop2d = new Cartesian2();
+const scratchBottom2d = new Cartesian2();
+const scratchAxis2D = new Cartesian2();
+const scratchMouseMoveVector = new Cartesian2();
+export function getVerticalMoveVector(
+ scene: Scene,
+ pickedPosition: Cartesian3,
+ endPosition: Cartesian2,
+ model: INGVCesiumModel,
+ result: Cartesian3 = new Cartesian3(),
+): Cartesian3 {
+ const cartPickedPosition = Cartographic.fromCartesian(
+ pickedPosition,
+ Ellipsoid.default,
+ scratchPickedPositionCart,
+ );
+ pickedPosition.clone(scratchPickedPosition);
+ const bottomPos = Cartesian3.fromRadians(
+ cartPickedPosition.longitude,
+ cartPickedPosition.latitude,
+ cartPickedPosition.height - model.id.dimensions.y,
+ );
+ scene.cartesianToCanvasCoordinates(scratchPickedPosition, scratchTop2d);
+ scene.cartesianToCanvasCoordinates(bottomPos, scratchBottom2d);
+ Cartesian2.subtract(scratchTop2d, scratchBottom2d, scratchAxis2D);
+ Cartesian2.subtract(endPosition, scratchTop2d, scratchMouseMoveVector);
+ const scalar2d =
+ Cartesian2.dot(scratchMouseMoveVector, scratchAxis2D) /
+ Cartesian2.dot(scratchAxis2D, scratchAxis2D);
+
+ const scalar3d =
+ getPixelSize(scene, model.boundingSphere) *
+ scalar2d *
+ model.id.dimensions.y;
+
+ Cartesian3.normalize(
+ getTranslationFromMatrix(model.modelMatrix),
+ scratchUpDirection,
+ );
+ return Cartesian3.multiplyByScalar(scratchUpDirection, scalar3d, result);
+}
+
+const scratchCameraRay = new Ray();
+const scratchRayPlane = new Cartesian3();
+export function getHorizontalMoveVector(
+ scene: Scene,
+ pickedPosition: Cartesian3,
+ endPosition: Cartesian2,
+ movePlane: Plane,
+ result: Cartesian3 = new Cartesian3(),
+): Cartesian3 | undefined {
+ const cameraRay = scene.camera.getPickRay(endPosition, scratchCameraRay);
+ if (!cameraRay) {
+ return undefined;
+ }
+ const nextPosition = IntersectionTests.rayPlane(
+ cameraRay,
+ movePlane,
+ scratchRayPlane,
+ );
+
+ if (!nextPosition) {
+ return undefined;
+ }
+
+ return Cartesian3.subtract(nextPosition, pickedPosition, result);
+}
+
+export function getPixelSize(
+ scene: Scene,
+ boundingSphere: BoundingSphere,
+): number {
+ return scene.camera.getPixelSize(
+ boundingSphere,
+ scene.drawingBufferWidth,
+ scene.drawingBufferHeight,
+ );
+}
+
+type GltfJson = {
+ bufferViews: {byteOffset: number; byteLength: number}[];
+ accessors: Record;
+ meshes: {
+ primitives: {
+ attributes: {
+ POSITION: number;
+ };
+ }[];
+ }[];
+};
+
+export function getDimensions(model: Model): Cartesian3 {
+ // @ts-expect-error loader is not part of API
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment
+ const json: GltfJson = model.loader.gltfJson;
+
+ const diameter = model.boundingSphere.radius * 2;
+ const min = new Cartesian3(diameter, diameter, diameter);
+ const max = new Cartesian3(
+ -Number.MAX_VALUE,
+ -Number.MAX_VALUE,
+ -Number.MAX_VALUE,
+ );
+
+ json.meshes.forEach(function (mesh) {
+ mesh.primitives.forEach(function (primitive) {
+ const positionAccessor = json.accessors[primitive.attributes.POSITION];
+
+ if (positionAccessor) {
+ if (positionAccessor.min) {
+ min.x = Math.min(min.x, positionAccessor.min[1]);
+ min.y = Math.min(min.y, positionAccessor.min[0]);
+ min.z = Math.min(min.z, positionAccessor.min[2]);
+ }
+ if (positionAccessor.max) {
+ max.x = Math.max(max.x, positionAccessor.max[1]);
+ max.y = Math.max(max.y, positionAccessor.max[0]);
+ max.z = Math.max(max.z, positionAccessor.max[2]);
+ }
+ }
+ });
+ });
+
+ return Cartesian3.subtract(max, min, new Cartesian3());
+}
diff --git a/src/plugins/cesium/localStore.ts b/src/plugins/cesium/localStore.ts
new file mode 100644
index 0000000..33334d4
--- /dev/null
+++ b/src/plugins/cesium/localStore.ts
@@ -0,0 +1,152 @@
+// todo Just for easier testing. Better place should be find and structure improved.
+
+import {Cartesian3, Matrix3, Matrix4, Quaternion} from '@cesium/engine';
+import type {INGVCesiumModel} from '../../interfaces/cesium/ingv-layers.js';
+
+export function storeBlobInIndexedDB(
+ dbName: string,
+ blob: Blob,
+ name: string,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbName, 2);
+
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ if (!db.objectStoreNames.contains('files')) {
+ db.createObjectStore('files');
+ }
+ };
+
+ request.onsuccess = () => {
+ const db = request.result;
+ const transaction = db.transaction('files', 'readwrite');
+ const store = transaction.objectStore('files');
+ store.put(blob, name);
+ transaction.oncomplete = () => {
+ console.log('Blob stored successfully');
+ resolve();
+ };
+ transaction.onerror = () => {
+ reject(new Error('Error storing blob'));
+ };
+ };
+
+ request.onerror = (event) => console.error('IndexedDB error:', event);
+ });
+}
+
+export function getBlobFromIndexedDB(
+ dbName: string,
+ name: string,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbName, 2);
+
+ request.onupgradeneeded = () => {
+ const db = request.result;
+ if (!db.objectStoreNames.contains('files')) {
+ db.createObjectStore('files');
+ }
+ };
+
+ request.onsuccess = () => {
+ const db = request.result;
+ const transaction = db.transaction('files', 'readonly');
+ const store = transaction.objectStore('files');
+ const getRequest = >store.get(name);
+
+ getRequest.onsuccess = () => {
+ const blob = getRequest.result;
+ if (blob) {
+ resolve(blob);
+ } else {
+ reject(new Error('Blob not found'));
+ }
+ };
+
+ getRequest.onerror = () =>
+ reject(new Error('Error retrieving blob from IndexedDB'));
+ };
+
+ request.onerror = () => reject(new Error('Error opening IndexedDB'));
+ });
+}
+
+export function deleteFromIndexedDB(
+ dbName: string,
+ name: string,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const request = indexedDB.open(dbName, 2);
+
+ request.onsuccess = () => {
+ const db = request.result;
+ const transaction = db.transaction('files', 'readwrite');
+ const store = transaction.objectStore('files');
+ const deleteRequest = store.delete(name);
+
+ deleteRequest.onsuccess = () => {
+ console.log(`Item with key "${name}" deleted successfully`);
+ resolve();
+ };
+
+ deleteRequest.onerror = () => {
+ console.error('Error deleting item from IndexedDB');
+ reject(new Error('Error deleting item from IndexedDB'));
+ };
+ };
+
+ request.onerror = () => {
+ console.error('Error opening IndexedDB');
+ reject(new Error('Error opening IndexedDB'));
+ };
+ });
+}
+
+export type StoredModel = {
+ name: string;
+ dimensions: number[];
+ translation: number[];
+ rotation: number[];
+ scale: number[];
+};
+
+export function updateModelsInLocalStore(
+ storeKey: string,
+ models: INGVCesiumModel[],
+): void {
+ const localStoreModels: StoredModel[] = [];
+ models.forEach((model) => {
+ const translation = Matrix4.getTranslation(
+ model.modelMatrix,
+ new Cartesian3(),
+ );
+ const scale = Matrix4.getScale(model.modelMatrix, new Cartesian3());
+ const rotation = Quaternion.fromRotationMatrix(
+ Matrix4.getRotation(model.modelMatrix, new Matrix3()),
+ );
+ localStoreModels.push({
+ name: model.id.name,
+ dimensions: [
+ model.id.dimensions.x,
+ model.id.dimensions.y,
+ model.id.dimensions.z,
+ ],
+ translation: [translation.x, translation.y, translation.z],
+ rotation: [rotation.x, rotation.y, rotation.z, rotation.w],
+ scale: [scale.x, scale.y, scale.z],
+ });
+ });
+ localStorage.setItem(storeKey, JSON.stringify(localStoreModels));
+}
+
+export function getStoredModels(storeKey: string): StoredModel[] {
+ if (!localStorage.getItem(storeKey)) return [];
+ try {
+ return JSON.parse(localStorage.getItem(storeKey));
+ } catch (e) {
+ console.error('Not possible to parse models from local storage', e);
+ return [];
+ }
+}
diff --git a/src/plugins/cesium/ngv-cesium-factories.ts b/src/plugins/cesium/ngv-cesium-factories.ts
index 349d243..1e1c70b 100644
--- a/src/plugins/cesium/ngv-cesium-factories.ts
+++ b/src/plugins/cesium/ngv-cesium-factories.ts
@@ -1,19 +1,26 @@
-import type {ImageryProvider} from '@cesium/engine';
+import {type ImageryProvider, PrimitiveCollection} from '@cesium/engine';
import {
Ion,
Math as CesiumMath,
CesiumWidget,
Cartesian3,
Model,
+ DataSourceCollection,
+ DataSourceDisplay,
+ Cartographic,
+ HeadingPitchRoll,
+ Transforms,
+ Ellipsoid,
} from '@cesium/engine';
import type {
INGVCesium3DTiles,
- INGVCesiumModel,
+ INGVCesiumModelConfig,
INGVCesiumAllTypes,
INGVCesiumImageryTypes,
INGVCesiumTerrain,
INGVIFC,
+ INGVCesiumModel,
} from '../../interfaces/cesium/ingv-layers.js';
import {
Cesium3DTileset,
@@ -24,6 +31,7 @@ import {
} from '@cesium/engine';
import type {IngvCesiumContext} from '../../interfaces/cesium/ingv-cesium-context.js';
import type {INGVCatalog} from '../../interfaces/cesium/ingv-catalog.js';
+import {getDimensions} from './interactionHelpers.js';
function withExtra(options: T, extra: Record): T {
if (!extra) {
@@ -51,10 +59,16 @@ export async function instantiateTerrain(
}
export async function instantiateModel(
- config: INGVCesiumModel,
+ config: INGVCesiumModelConfig,
extraOptions?: Record,
-): Promise {
- return Model.fromGltfAsync(withExtra(config.options, extraOptions));
+): Promise {
+ const model: INGVCesiumModel = await Model.fromGltfAsync(
+ withExtra(config.options, extraOptions),
+ );
+ model.readyEvent.addEventListener(() => {
+ model.id.dimensions = getDimensions(model);
+ });
+ return model;
}
export async function instantiate3dTileset(
@@ -151,7 +165,7 @@ export function is3dTilesetConfig(
export function isModelConfig(
config: INGVCesiumAllTypes,
-): config is INGVCesiumModel {
+): config is INGVCesiumModelConfig {
return config?.type === 'model';
}
@@ -202,7 +216,14 @@ export async function initCesiumWidget(
container: HTMLDivElement,
cesiumContext: IngvCesiumContext,
modelCallback: (name: string, model: Model) => void,
-): Promise {
+): Promise<{
+ viewer: CesiumWidget;
+ dataSourceCollection: DataSourceCollection;
+ primitiveCollections: {
+ models: PrimitiveCollection;
+ tiles3d: PrimitiveCollection;
+ };
+}> {
modelCallback =
modelCallback ||
(() => {
@@ -248,6 +269,29 @@ export async function initCesiumWidget(
Object.assign({}, cesiumContext.widgetOptions),
);
+ if (cesiumContext.globeOptions) {
+ Object.assign(viewer.scene.globe, cesiumContext.globeOptions);
+ }
+
+ const primitiveCollections = {
+ models: new PrimitiveCollection(),
+ tiles3d: new PrimitiveCollection(),
+ };
+
+ viewer.scene.primitives.add(primitiveCollections.models);
+ viewer.scene.primitives.add(primitiveCollections.tiles3d);
+
+ const dataSourceCollection = new DataSourceCollection();
+ const dataSourceDisplay = new DataSourceDisplay({
+ scene: viewer.scene,
+ dataSourceCollection: dataSourceCollection,
+ });
+ const clock = viewer.clock;
+ // todo: check if OK
+ clock.onTick.addEventListener(() => {
+ dataSourceDisplay.update(clock.currentTime);
+ });
+
const stuffToDo: Promise[] = [];
if (cesiumContext.layers.terrain) {
const name = cesiumContext.layers.terrain;
@@ -272,14 +316,14 @@ export async function initCesiumWidget(
stuffToDo.push(
instantiate3dTileset(config, cesiumContext.layerOptions[name]).then(
(tileset) => {
- viewer.scene.primitives.add(tileset);
+ primitiveCollections.tiles3d.add(tileset);
},
),
);
});
- const modelPromises = cesiumContext.layers.models?.map(async (name) => {
- let config = resolvedLayers[name];
+ const modelPromises = cesiumContext.layers.models?.map(async (path) => {
+ let config = resolvedLayers[path];
let toRevokeUrl: string;
if (isIFCConfig(config)) {
@@ -300,16 +344,27 @@ export async function initCesiumWidget(
console.log('IFC transformed to glTF', metadata, coordinationMatrix);
const glbBlob = new Blob([glb]);
toRevokeUrl = URL.createObjectURL(glbBlob);
- const modelConfig: INGVCesiumModel = {
+ const modelConfig: INGVCesiumModelConfig = {
type: 'model',
options: Object.assign({}, modelOptions, {
url: toRevokeUrl,
+ id: {
+ name: ifcUrl,
+ },
}),
+ height: config.height,
+ position: config.position,
+ rotation: config.rotation,
};
config = modelConfig;
+ } else if (isModelConfig(config)) {
+ config = {
+ ...config,
+ options: {...config.options, id: {name: config.options.url}},
+ };
}
if (isModelConfig(config)) {
- const bmConfig: Omit = {
+ const bmConfig: Omit = {
scene: viewer.scene,
gltfCallback(gltf) {
// FIXME: here we can enable animations, ...
@@ -318,16 +373,29 @@ export async function initCesiumWidget(
},
// heightReference: HeightReference.CLAMP_TO_GROUND,
};
+ const modelMatrix = Transforms.headingPitchRollToFixedFrame(
+ Cartographic.toCartesian(
+ Cartographic.fromDegrees(
+ config.position[0],
+ config.position[1],
+ config.height,
+ ),
+ ),
+ new HeadingPitchRoll(CesiumMath.toRadians(config.rotation)),
+ Ellipsoid.WGS84,
+ Transforms.localFrameToFixedFrameGenerator('north', 'west'),
+ );
stuffToDo.push(
instantiateModel(
config,
- Object.assign(bmConfig, cesiumContext.layerOptions[name]),
+ Object.assign(bmConfig, cesiumContext.layerOptions[path], {
+ modelMatrix,
+ }),
)
.then(
(model) => {
- console.log('Got model!', config);
- modelCallback(name, model);
- viewer.scene.primitives.add(model);
+ modelCallback(path, model);
+ primitiveCollections.models.add(model);
},
(e) => {
console.error('o', e);
@@ -374,5 +442,5 @@ export async function initCesiumWidget(
duration: 0,
});
- return viewer;
+ return {viewer, dataSourceCollection, primitiveCollections};
}
diff --git a/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts
new file mode 100644
index 0000000..0a369e9
--- /dev/null
+++ b/src/plugins/cesium/ngv-plugin-cesium-model-interact.ts
@@ -0,0 +1,453 @@
+import {html, LitElement} from 'lit';
+import type {HTMLTemplateResult, PropertyValues} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import type {
+ CesiumWidget,
+ DataSource,
+ PrimitiveCollection,
+ DataSourceCollection,
+ Cartesian2,
+} from '@cesium/engine';
+import {
+ Model,
+ Cartesian3,
+ Color,
+ Matrix4,
+ Plane,
+ ScreenSpaceEventHandler,
+ ScreenSpaceEventType,
+ Quaternion,
+ CustomDataSource,
+ Ellipsoid,
+ TranslationRotationScale,
+ Entity,
+ ColorMaterialProperty,
+} from '@cesium/engine';
+import {instantiateModel} from './ngv-cesium-factories.js';
+import type {StoredModel} from './localStore.js';
+import {
+ deleteFromIndexedDB,
+ getBlobFromIndexedDB,
+ getStoredModels,
+ updateModelsInLocalStore,
+} from './localStore.js';
+import '../ui/ngv-layer-details.js';
+import '../ui/ngv-layers-list.js';
+import type {BBoxStyles} from './interactionHelpers.js';
+import {
+ getHorizontalMoveVector,
+ getTranslationFromMatrix,
+ getVerticalMoveVector,
+ rotate,
+ scale,
+ showModelBBox,
+} from './interactionHelpers.js';
+import type {INGVCesiumModel} from '../../interfaces/cesium/ingv-layers.js';
+
+type GrabType = 'side' | 'top' | 'edge' | 'corner' | undefined;
+
+@customElement('ngv-plugin-cesium-model-interact')
+export class NgvPluginCesiumModelInteract extends LitElement {
+ @property({type: Object})
+ private viewer: CesiumWidget;
+ @property({type: Object})
+ private primitiveCollection: PrimitiveCollection;
+ @property({type: Object})
+ private dataSourceCollection: DataSourceCollection;
+ @property({type: Object})
+ private bboxStyle: BBoxStyles | undefined;
+ @property({type: Object})
+ private storeOptions?: {
+ localStoreKey: string;
+ indexDbName: string;
+ };
+ @property({type: Object})
+ private options?: {
+ listTitle: string;
+ };
+ @state()
+ private cursor:
+ | 'default'
+ | 'move'
+ | 'pointer'
+ | 'ns-resize'
+ | 'ew-resize'
+ | 'nesw-resize' = 'default';
+ @state()
+ private chosenModel: INGVCesiumModel | undefined;
+ @state()
+ private position: Cartesian3 = new Cartesian3();
+ @state()
+ private models: INGVCesiumModel[] = [];
+ private eventHandler: ScreenSpaceEventHandler | undefined;
+ private sidePlanesDataSource: DataSource | undefined;
+ private topDownPlanesDataSource: DataSource | undefined;
+ private edgeLinesDataSource: DataSource | undefined;
+ private cornerPointsDataSource: DataSource | undefined;
+ private moveStart: Cartesian3 = new Cartesian3();
+ private moveStep: Cartesian3 = new Cartesian3();
+ private pickedPointOffset: Cartesian3 = new Cartesian3();
+ private dragStart: boolean = false;
+ private movePlane: Plane | undefined;
+ private grabType: GrabType;
+ private hoveredEdge: Entity | undefined;
+
+ initEvents(): void {
+ this.eventHandler = new ScreenSpaceEventHandler(this.viewer.canvas);
+ this.eventHandler.setInputAction(
+ (evt: ScreenSpaceEventHandler.MotionEvent) => this.onMouseMove(evt),
+ ScreenSpaceEventType.MOUSE_MOVE,
+ );
+ this.eventHandler.setInputAction(
+ (evt: ScreenSpaceEventHandler.PositionedEvent) => this.onClick(evt),
+ ScreenSpaceEventType.LEFT_CLICK,
+ );
+ this.eventHandler.setInputAction(
+ (evt: ScreenSpaceEventHandler.PositionedEvent) => this.onLeftDown(evt),
+ ScreenSpaceEventType.LEFT_DOWN,
+ );
+ this.eventHandler.setInputAction(
+ () => this.onLeftUp(),
+ ScreenSpaceEventType.LEFT_UP,
+ );
+
+ this.primitiveCollection.primitiveAdded.addEventListener(() => {
+ this.onPrimitivesChanged();
+ });
+ this.primitiveCollection.primitiveRemoved.addEventListener(
+ (p: INGVCesiumModel) => {
+ if (this.storeOptions) {
+ deleteFromIndexedDB(this.storeOptions.indexDbName, p.id.name)
+ .then(() => this.onPrimitivesChanged())
+ .catch((e) => console.error(e));
+ } else {
+ this.onPrimitivesChanged();
+ }
+ },
+ );
+ }
+
+ removeEvents(): void {
+ if (this.cursor !== 'default') {
+ this.viewer.canvas.style.cursor = 'default';
+ }
+ if (this.eventHandler) {
+ this.eventHandler.destroy();
+ this.eventHandler = null;
+ }
+ }
+
+ onPrimitivesChanged(): void {
+ this.models = [];
+ for (let i = 0; i < this.primitiveCollection.length; i++) {
+ const model = this.primitiveCollection.get(i) as INGVCesiumModel;
+ if (model instanceof Model) {
+ this.models.push(model);
+ }
+ }
+ if (this.storeOptions) {
+ updateModelsInLocalStore(this.storeOptions.localStoreKey, this.models);
+ }
+ }
+
+ onClick(evt: ScreenSpaceEventHandler.PositionedEvent): void {
+ const model: Model | undefined = this.pickModel(evt.position);
+ if (model) {
+ if (!this.chosenModel) {
+ this.chosenModel = model;
+ Matrix4.getTranslation(this.chosenModel.modelMatrix, this.position);
+ showModelBBox(
+ {
+ topDownPlanesDataSource: this.topDownPlanesDataSource,
+ sidePlanesDataSource: this.sidePlanesDataSource,
+ edgeLinesDataSource: this.edgeLinesDataSource,
+ cornerPointsDataSource: this.cornerPointsDataSource,
+ },
+ this.chosenModel,
+ this.viewer.scene,
+ this.bboxStyle,
+ );
+ }
+ }
+ }
+
+ onLeftDown(evt: ScreenSpaceEventHandler.PositionedEvent): void {
+ this.grabType = this.pickGrabType(evt.position);
+ if (this.grabType) {
+ this.viewer.scene.screenSpaceCameraController.enableInputs = false;
+ this.viewer.scene.pickPosition(evt.position, this.moveStart);
+ this.dragStart = true;
+
+ const normal = Ellipsoid.WGS84.geodeticSurfaceNormal(this.moveStart);
+ this.movePlane = Plane.fromPointNormal(this.moveStart, normal);
+ }
+ }
+ onLeftUp(): void {
+ if (this.grabType) {
+ this.viewer.scene.screenSpaceCameraController.enableInputs = true;
+ this.grabType = undefined;
+ }
+ }
+
+ onMouseMove(evt: ScreenSpaceEventHandler.MotionEvent): void {
+ if (this.grabType && this.chosenModel) {
+ const endPosition = this.viewer.scene.pickPosition(evt.endPosition);
+ if (!endPosition) return;
+
+ if (this.grabType === 'edge') {
+ rotate(
+ evt.startPosition,
+ evt.endPosition,
+ this.chosenModel.modelMatrix,
+ );
+ return;
+ }
+ if (this.grabType === 'corner') {
+ scale(evt.startPosition, evt.endPosition, this.chosenModel.modelMatrix);
+ return;
+ }
+ if (this.dragStart) {
+ Cartesian3.subtract(
+ endPosition,
+ getTranslationFromMatrix(this.chosenModel.modelMatrix),
+ this.pickedPointOffset,
+ );
+ this.dragStart = false;
+ }
+
+ const pickedPosition = Cartesian3.add(
+ this.position,
+ this.pickedPointOffset,
+ new Cartesian3(),
+ );
+
+ if (this.grabType === 'top') {
+ getVerticalMoveVector(
+ this.viewer.scene,
+ pickedPosition,
+ evt.endPosition,
+ this.chosenModel,
+ this.moveStep,
+ );
+ } else if (this.grabType === 'side') {
+ getHorizontalMoveVector(
+ this.viewer.scene,
+ pickedPosition,
+ evt.endPosition,
+ this.movePlane,
+ this.moveStep,
+ );
+ }
+
+ Cartesian3.add(this.position, this.moveStep, this.position);
+
+ Matrix4.setTranslation(
+ this.chosenModel.modelMatrix,
+ this.position,
+ this.chosenModel.modelMatrix,
+ );
+
+ return;
+ }
+ this.updateCursor(evt.endPosition);
+ }
+
+ updateCursor(position: Cartesian2): void {
+ const model: Model | undefined = this.pickModel(position);
+ const isSidePlane = !!this.pickEntity(position, this.sidePlanesDataSource);
+ const isTopPlane = !!this.pickEntity(
+ position,
+ this.topDownPlanesDataSource,
+ );
+ const edgeEntity = this.pickEntity(position, this.edgeLinesDataSource);
+ const isCorner = !!this.pickEntity(position, this.cornerPointsDataSource);
+ if (model && !this.chosenModel) {
+ if (this.cursor !== 'pointer') {
+ this.viewer.canvas.style.cursor = this.cursor = 'pointer';
+ }
+ } else if (isSidePlane) {
+ if (this.cursor !== 'move') {
+ this.viewer.canvas.style.cursor = this.cursor = 'move';
+ }
+ } else if (isTopPlane) {
+ if (this.cursor !== 'ns-resize') {
+ this.viewer.canvas.style.cursor = this.cursor = 'ns-resize';
+ }
+ } else if (isCorner) {
+ if (this.cursor !== 'nesw-resize') {
+ this.viewer.canvas.style.cursor = this.cursor = 'nesw-resize';
+ }
+ } else if (edgeEntity) {
+ if (this.cursor !== 'ew-resize') {
+ this.viewer.canvas.style.cursor = this.cursor = 'ew-resize';
+ }
+ if (!this.hoveredEdge) {
+ this.hoveredEdge = edgeEntity;
+ this.hoveredEdge.polyline.material = new ColorMaterialProperty(
+ Color.WHITE.withAlpha(0.9),
+ );
+ }
+ } else if (this.cursor !== 'default') {
+ this.viewer.canvas.style.cursor = this.cursor = 'default';
+ }
+ if (this.hoveredEdge && !edgeEntity) {
+ this.hoveredEdge.polyline.material = new ColorMaterialProperty(
+ Color.WHITE.withAlpha(0.3),
+ );
+ this.hoveredEdge = undefined;
+ }
+ }
+
+ pickModel(position: Cartesian2): Model | undefined {
+ const pickedObject: {primitive: Model | undefined} = <
+ {primitive: Model | undefined}
+ >this.viewer.scene.pick(position);
+ return pickedObject?.primitive &&
+ this.primitiveCollection.contains(pickedObject.primitive)
+ ? pickedObject.primitive
+ : undefined;
+ }
+
+ pickEntity(position: Cartesian2, dataSource: DataSource): Entity | undefined {
+ const obj: {id: Entity | undefined} = <{id: Entity | undefined}>(
+ this.viewer.scene.pick(position)
+ );
+ return obj?.id &&
+ obj.id instanceof Entity &&
+ dataSource.entities.contains(obj.id)
+ ? obj?.id
+ : undefined;
+ }
+
+ pickGrabType(position: Cartesian2): GrabType | undefined {
+ const pickedObject: {id: Entity | undefined} = <{id: Entity | undefined}>(
+ this.viewer.scene.pick(position)
+ );
+ if (!pickedObject?.id?.id) return undefined;
+ if (this.sidePlanesDataSource.entities.contains(pickedObject.id)) {
+ return 'side';
+ } else if (
+ this.topDownPlanesDataSource.entities.contains(pickedObject.id)
+ ) {
+ return 'top';
+ } else if (this.edgeLinesDataSource.entities.contains(pickedObject.id)) {
+ return 'edge';
+ } else if (this.cornerPointsDataSource.entities.contains(pickedObject.id)) {
+ return 'corner';
+ }
+ return undefined;
+ }
+
+ initDataSources(): void {
+ this.dataSourceCollection
+ .add(new CustomDataSource())
+ .then((dataSource) => (this.sidePlanesDataSource = dataSource))
+ .catch((e) => console.error(e));
+ this.dataSourceCollection
+ .add(new CustomDataSource())
+ .then((dataSource) => (this.topDownPlanesDataSource = dataSource))
+ .catch((e) => console.error(e));
+ this.dataSourceCollection
+ .add(new CustomDataSource())
+ .then((dataSource) => (this.edgeLinesDataSource = dataSource))
+ .catch((e) => console.error(e));
+ this.dataSourceCollection
+ .add(new CustomDataSource())
+ .then((dataSource) => (this.cornerPointsDataSource = dataSource))
+ .catch((e) => console.error(e));
+ }
+
+ async initModelsAndEvents(): Promise {
+ const models = this.storeOptions
+ ? getStoredModels(this.storeOptions.localStoreKey)
+ : undefined;
+ if (models?.length) {
+ await Promise.all(
+ models.map(async (m: StoredModel) => {
+ const blob = await getBlobFromIndexedDB(
+ this.storeOptions.indexDbName,
+ m.name,
+ );
+ const model = await instantiateModel({
+ type: 'model',
+ options: {
+ url: URL.createObjectURL(blob),
+ scene: this.viewer.scene,
+ modelMatrix: Matrix4.fromTranslationRotationScale(
+ new TranslationRotationScale(
+ new Cartesian3(...m.translation),
+ new Quaternion(...m.rotation),
+ new Cartesian3(...m.scale),
+ ),
+ ),
+ id: {
+ name: m.name,
+ dimensions: new Cartesian3(...Object.values(m.dimensions)),
+ },
+ },
+ });
+ this.primitiveCollection.add(model);
+ }),
+ );
+ this.onPrimitivesChanged();
+ this.initEvents();
+ this.viewer.scene.requestRender();
+ } else {
+ this.initEvents();
+ }
+ }
+
+ firstUpdated(_changedProperties: PropertyValues): void {
+ this.initModelsAndEvents().catch((e) => console.error(e));
+ this.initDataSources();
+ super.firstUpdated(_changedProperties);
+ }
+
+ protected shouldUpdate(): boolean {
+ return !!this.viewer && !!this.primitiveCollection;
+ }
+ render(): HTMLTemplateResult | string {
+ if (!this.chosenModel && !this.models?.length) return '';
+ return this.chosenModel
+ ? html` {
+ this.chosenModel = undefined;
+ this.sidePlanesDataSource.entities.removeAll();
+ this.topDownPlanesDataSource.entities.removeAll();
+ this.edgeLinesDataSource.entities.removeAll();
+ this.cornerPointsDataSource.entities.removeAll();
+ this.onPrimitivesChanged();
+ }}"
+ >`
+ : html` {
+ return {name: m.id.name};
+ })}
+ @remove="${(evt: {detail: number}) => {
+ const model = this.primitiveCollection.get(
+ evt.detail,
+ ) as INGVCesiumModel;
+ if (model) this.primitiveCollection.remove(model);
+ }}"
+ @zoom="${(evt: {detail: number}) => {
+ const model = this.primitiveCollection.get(
+ evt.detail,
+ ) as INGVCesiumModel;
+ this.viewer.camera.flyToBoundingSphere(model.boundingSphere, {
+ duration: 2,
+ });
+ }}"
+ >`;
+ }
+
+ disconnectedCallback(): void {
+ this.removeEvents();
+ super.disconnectedCallback();
+ }
+}
diff --git a/src/plugins/cesium/ngv-plugin-cesium-upload.ts b/src/plugins/cesium/ngv-plugin-cesium-upload.ts
new file mode 100644
index 0000000..d1bbd1f
--- /dev/null
+++ b/src/plugins/cesium/ngv-plugin-cesium-upload.ts
@@ -0,0 +1,137 @@
+import type {CesiumWidget, PrimitiveCollection} from '@cesium/engine';
+import {
+ Cartesian3,
+ Cartographic,
+ HeadingPitchRoll,
+ Math as CesiumMath,
+ Matrix4,
+ Quaternion,
+ ScreenSpaceEventHandler,
+ ScreenSpaceEventType,
+ Transforms,
+ TranslationRotationScale,
+} from '@cesium/engine';
+import type {HTMLTemplateResult} from 'lit';
+import {html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+import '../ui/ngv-upload.js';
+import {instantiateModel} from './ngv-cesium-factories.js';
+import type {FileUploadDetails} from '../ui/ngv-upload.js';
+import {storeBlobInIndexedDB, updateModelsInLocalStore} from './localStore.js';
+import type {INGVCesiumModel} from '../../interfaces/cesium/ingv-layers.js';
+
+const cartographicScratch = new Cartographic();
+
+@customElement('ngv-plugin-cesium-upload')
+export class NgvPluginCesiumUpload extends LitElement {
+ @property({type: Object})
+ public viewer: CesiumWidget;
+ @property({type: Object})
+ public primitiveCollection: PrimitiveCollection;
+ @property({type: Object})
+ public storeOptions?: {
+ localStoreKey: string;
+ indexDbName: string;
+ };
+ private eventHandler: ScreenSpaceEventHandler | null = null;
+ private uploadedModel: INGVCesiumModel | undefined;
+
+ async upload(fileDetails: FileUploadDetails): Promise {
+ const response = await fetch(fileDetails.url);
+ const arrayBuffer = await response.arrayBuffer();
+
+ const modelMatrix = Matrix4.fromTranslationRotationScale(
+ new TranslationRotationScale(
+ Cartesian3.ZERO,
+ Quaternion.fromHeadingPitchRoll(
+ new HeadingPitchRoll(CesiumMath.toRadians(90), 0, 0),
+ ),
+ ),
+ );
+
+ this.uploadedModel = await instantiateModel({
+ type: 'model',
+ options: {
+ url: fileDetails.url,
+ scene: this.viewer.scene,
+ modelMatrix,
+ id: {
+ name: fileDetails.name,
+ dimensions: Cartesian3.ZERO,
+ },
+ },
+ });
+ if (this.storeOptions) {
+ await storeBlobInIndexedDB(
+ this.storeOptions.indexDbName,
+ new Blob([arrayBuffer]),
+ fileDetails.name,
+ );
+ }
+ this.primitiveCollection.add(this.uploadedModel);
+ this.viewer.scene.requestRender();
+ this.showControls();
+ }
+
+ showControls(): void {
+ this.eventHandler = new ScreenSpaceEventHandler(this.viewer.canvas);
+ this.eventHandler.setInputAction(
+ (evt: ScreenSpaceEventHandler.MotionEvent) => this.onMouseMove(evt),
+ ScreenSpaceEventType.MOUSE_MOVE,
+ );
+ this.eventHandler.setInputAction(
+ this.onClick.bind(this),
+ ScreenSpaceEventType.LEFT_CLICK,
+ );
+ this.viewer.canvas.style.cursor = 'move';
+ }
+
+ onClick(): void {
+ this.viewer.canvas.style.cursor = 'default';
+ this.eventHandler.destroy();
+ this.eventHandler = null;
+ const models: INGVCesiumModel[] = [];
+ for (let i = 0; i < this.primitiveCollection.length; i++) {
+ models.push(this.primitiveCollection.get(i));
+ }
+ if (this.storeOptions) {
+ updateModelsInLocalStore(this.storeOptions.localStoreKey, models);
+ }
+ }
+
+ onMouseMove(event: ScreenSpaceEventHandler.MotionEvent): void {
+ const position = this.viewer.scene.pickPosition(event.endPosition);
+ const cart = Cartographic.fromCartesian(
+ position,
+ this.viewer.scene.ellipsoid,
+ cartographicScratch,
+ );
+ const altitude = this.viewer.scene.globe.getHeight(cart);
+ cart.height = altitude || 0;
+ Cartographic.toCartesian(cart, this.viewer.scene.ellipsoid, position);
+
+ this.uploadedModel.modelMatrix =
+ Transforms.eastNorthUpToFixedFrame(position);
+ }
+
+ protected shouldUpdate(): boolean {
+ return !!this.viewer && !!this.primitiveCollection;
+ }
+
+ render(): HTMLTemplateResult {
+ return html` {
+ this.upload(evt.detail).catch((e) =>
+ console.error(`Upload error: ${e}`),
+ );
+ }}"
+ >`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ngv-plugin-cesium-upload': NgvPluginCesiumUpload;
+ }
+}
diff --git a/src/plugins/cesium/ngv-plugin-cesium-widget.ts b/src/plugins/cesium/ngv-plugin-cesium-widget.ts
index 7bc098a..20ca80d 100644
--- a/src/plugins/cesium/ngv-plugin-cesium-widget.ts
+++ b/src/plugins/cesium/ngv-plugin-cesium-widget.ts
@@ -5,12 +5,27 @@ import {customElement, property, query} from 'lit/decorators.js';
// @ts-expect-error Vite specific ?inline parameter
import style from '@cesium/engine/Source/Widget/CesiumWidget.css?inline';
import type {IngvCesiumContext} from '../../interfaces/cesium/ingv-cesium-context.js';
-import type {CesiumWidget, Model} from '@cesium/engine';
+import type {
+ CesiumWidget,
+ DataSourceCollection,
+ Model,
+ PrimitiveCollection,
+} from '@cesium/engine';
import {initCesiumWidget} from './ngv-cesium-factories.js';
+export type ViewerInitializedDetails = {
+ viewer: CesiumWidget;
+ dataSourceCollection: DataSourceCollection;
+ primitiveCollections: {
+ models: PrimitiveCollection;
+ tiles3d: PrimitiveCollection;
+ };
+};
+
@customElement('ngv-plugin-cesium-widget')
export class NgvPluginCesiumWidget extends LitElement {
public viewer: CesiumWidget;
+ public dataSourceCollection: DataSourceCollection;
static styles = [
unsafeCSS(style),
@@ -41,14 +56,21 @@ export class NgvPluginCesiumWidget extends LitElement {
private element: HTMLDivElement;
private async initCesiumViewer(): Promise {
- this.viewer = await initCesiumWidget(
- this.element,
- this.cesiumContext,
- this.modelCallback,
- );
+ const {viewer, dataSourceCollection, primitiveCollections} =
+ await initCesiumWidget(
+ this.element,
+ this.cesiumContext,
+ this.modelCallback,
+ );
+ this.viewer = viewer;
+ this.dataSourceCollection = dataSourceCollection;
this.dispatchEvent(
- new CustomEvent('viewerInitialized', {
- detail: this.viewer,
+ new CustomEvent('viewerInitialized', {
+ detail: {
+ viewer,
+ dataSourceCollection,
+ primitiveCollections,
+ },
}),
);
}
diff --git a/src/plugins/ui/ngv-layer-details.ts b/src/plugins/ui/ngv-layer-details.ts
new file mode 100644
index 0000000..1d90dcc
--- /dev/null
+++ b/src/plugins/ui/ngv-layer-details.ts
@@ -0,0 +1,65 @@
+import {css, html, LitElement} from 'lit';
+import type {HTMLTemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+export type LayerDetails = {
+ name: string;
+};
+
+@customElement('ngv-layer-details')
+export class NgvLayerDetails extends LitElement {
+ @property({type: Object})
+ private layer: LayerDetails;
+
+ static styles = css`
+ .info {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ z-index: 1;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 10px;
+ gap: 10px;
+ border-radius: 4px;
+ border: 1px solid rgba(0, 0, 0, 0.16);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+ }
+
+ button,
+ input[type='text'] {
+ border-radius: 4px;
+ padding: 0 16px;
+ height: 40px;
+ cursor: pointer;
+ background-color: white;
+ border: 1px solid rgba(0, 0, 0, 0.16);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+ transition: background-color 200ms;
+ }
+
+ input[type='text'] {
+ cursor: text;
+ }
+ `;
+
+ render(): HTMLTemplateResult | string {
+ if (!this.layer) return '';
+ return html`
+ ${this.layer.name}
+
+
`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ngv-layer-details': NgvLayerDetails;
+ }
+}
diff --git a/src/plugins/ui/ngv-layers-list.ts b/src/plugins/ui/ngv-layers-list.ts
new file mode 100644
index 0000000..a5cfa40
--- /dev/null
+++ b/src/plugins/ui/ngv-layers-list.ts
@@ -0,0 +1,99 @@
+import {css, html, LitElement} from 'lit';
+import type {HTMLTemplateResult} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+export type LayerListItem = {
+ name: string;
+};
+
+export type LayerListOptions = {
+ title?: string;
+ showDeleteBtns?: boolean;
+ showZoomBtns?: boolean;
+};
+
+@customElement('ngv-layers-list')
+export class NgvLayersList extends LitElement {
+ @property({type: Object})
+ private layers: LayerListItem[];
+ @property({type: Object})
+ private options?: LayerListOptions;
+
+ static styles = css`
+ .list {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ z-index: 1;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 10px;
+ gap: 10px;
+ border-radius: 4px;
+ border: 1px solid rgba(0, 0, 0, 0.16);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+ }
+
+ .item {
+ text-overflow: ellipsis;
+ display: flex;
+ align-items: center;
+ column-gap: 10px;
+ }
+
+ .item span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ button {
+ border-radius: 4px;
+ padding: 0 16px;
+ height: 40px;
+ cursor: pointer;
+ background-color: white;
+ border: 1px solid rgba(0, 0, 0, 0.16);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+ transition: background-color 200ms;
+ }
+ `;
+
+ render(): HTMLTemplateResult | string {
+ if (!this.layers?.length) return '';
+ return html` ${this.options.title}
+
+ ${this.layers.map(
+ (l, i) =>
+ html`
+ ${this.options?.showZoomBtns
+ ? html``
+ : ''}
+ ${l.name}
+ ${this.options?.showDeleteBtns
+ ? html``
+ : ''}
+
`,
+ )}
+
`;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ngv-layers-list': NgvLayersList;
+ }
+}
diff --git a/src/plugins/ui/ngv-upload.ts b/src/plugins/ui/ngv-upload.ts
new file mode 100644
index 0000000..903cdb3
--- /dev/null
+++ b/src/plugins/ui/ngv-upload.ts
@@ -0,0 +1,137 @@
+import {customElement, property, query, state} from 'lit/decorators.js';
+import type {HTMLTemplateResult} from 'lit';
+import {css, html, LitElement} from 'lit';
+import {msg} from '@lit/localize';
+
+export type NgvUploadOptions = {
+ accept?: string;
+ mainBtnText?: string;
+ urlInput?: boolean;
+ urlPlaceholderText?: string;
+ fileInput?: boolean;
+ uploadBtnText?: string;
+};
+
+export type FileUploadDetails = {
+ url: string;
+ name: string;
+};
+
+@customElement('ngv-upload')
+export class NgvUpload extends LitElement {
+ @property({type: Object}) options: NgvUploadOptions;
+ @state() showPopup = false;
+ @state() fileDetails: FileUploadDetails | undefined;
+ @query('input[type="file"]') fileInput: HTMLInputElement;
+
+ static styles = css`
+ .upload-popup {
+ background-color: white;
+ display: flex;
+ flex-direction: column;
+ margin-left: auto;
+ margin-right: auto;
+ padding: 10px;
+ gap: 10px;
+ border-radius: 4px;
+ border: 1px solid rgba(0, 0, 0, 0.16);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+ }
+
+ button,
+ input[type='text'],
+ input[type='file']::file-selector-button {
+ border-radius: 4px;
+ padding: 0 16px;
+ height: 40px;
+ cursor: pointer;
+ background-color: white;
+ border: 1px solid rgba(0, 0, 0, 0.16);
+ box-shadow: 0 1px 0 rgba(0, 0, 0, 0.05);
+ margin-right: 16px;
+ transition: background-color 200ms;
+ }
+
+ input[type='text'] {
+ cursor: text;
+ }
+
+ input[type='file'] {
+ background-color: #f3f4f6;
+ cursor: pointer;
+ }
+
+ input[type='file']::file-selector-button:active {
+ background-color: #e5e7eb;
+ }
+ `;
+
+ async onFileUpload(file: File): Promise {
+ const arrayBufer = await file.arrayBuffer();
+ const blob = new Blob([arrayBufer]);
+ this.fileDetails = {
+ url: URL.createObjectURL(blob),
+ name: file.name,
+ };
+ }
+
+ upload(): void {
+ this.dispatchEvent(
+ new CustomEvent('uploaded', {
+ detail: this.fileDetails,
+ }),
+ );
+ this.fileInput.value = '';
+ this.showPopup = false;
+ }
+
+ override willUpdate(): void {
+ let options: NgvUploadOptions = {
+ mainBtnText: msg('Upload'),
+ uploadBtnText: msg('Upload'),
+ urlInput: true,
+ fileInput: true,
+ urlPlaceholderText: msg('Put file URL here'),
+ accept: '*/*',
+ };
+ if (this.options) {
+ options = {...options, ...this.options};
+ }
+ this.options = options;
+ }
+
+ render(): HTMLTemplateResult {
+ return html` `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'ngv-upload': NgvUpload;
+ }
+}
diff --git a/src/structure/ngv-structure-header.ts b/src/structure/ngv-structure-header.ts
index 66da786..d5029ae 100644
--- a/src/structure/ngv-structure-header.ts
+++ b/src/structure/ngv-structure-header.ts
@@ -32,10 +32,13 @@ export class NgvStructureHeader extends LitElement {
.searchContext=${headerConfig.searchContext}
>`
: ''}
-
-
+ ${this.config.authContext
+ ? html`
+
+ `
+ : ''}