Skip to content

Commit

Permalink
feat: add Data Models support for 360 images (#3921)
Browse files Browse the repository at this point in the history
* refactor: refactor out descriptor provider from 360 image provider and compose the functionality

* feat: add Cdf360DataModelsDescriptorProvider for fetching 360 image collection from data models

* feat: add combined descriptor provider for data models and events

* feat: expose endpoint for adding 360 image collections from data models

* chore: update API spec and throttle file ids retrival

* fix: add cursoring support

* feat: add stations support

* fix: add FDM loading capability to UI

* chore: add type tests for DMS SDK

* chore: add basic unit test

* fix: file rename and visual test types

* chore: rename UniqueIdentifier to Image360DataModelIdentifier

* chore: smaller improvements

---------

Co-authored-by: Håkon Flatval <[email protected]>
Co-authored-by: Håkon Flatval <[email protected]>
  • Loading branch information
3 people authored Dec 4, 2023
1 parent 026bcba commit 74eb8a9
Show file tree
Hide file tree
Showing 17 changed files with 1,409 additions and 461 deletions.
143 changes: 93 additions & 50 deletions examples/src/utils/Image360UI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class Image360UI {

private params = {
siteId: getSiteIdFromUrl() ?? '',
space: getSpaceFromUrl() ?? '',
add: () => this.add360ImageSet(),
remove: () => this.remove360ImageSet(),
premultipliedRotation: false,
Expand All @@ -54,6 +55,10 @@ export class Image360UI {
z: 0
};

private dataSource: { type: 'events' | 'dataModels' } = {
type: getSiteIdFromUrl() !== null && getSpaceFromUrl() === null ? 'events' : 'dataModels'
};

private rotation = {
x: 0,
y: 0,
Expand All @@ -77,13 +82,27 @@ export class Image360UI {
};

constructor(viewer: Cognite3DViewer, gui: dat.GUI) {
const { params, imageRevisions, _collections: collections, selectedEntity } = this;
this.viewer = viewer;
this.gui = gui;

const optionsFolder = this.gui.addFolder('Add Options');
const optionsFolder = this.gui.addFolder('Add Options (Events)');
optionsFolder.hide();
const optionsFolderFdm = this.gui.addFolder('Add Options (Data Models)');
this.gui
.add(this.dataSource, 'type', ['events', 'dataModels'])
.name('Data source')
.onChange((a: 'events' | 'dataModels') => {
if (a === 'events') {
optionsFolder.show();
optionsFolderFdm.hide();
} else {
optionsFolder.hide();
optionsFolderFdm.show();
}
});

optionsFolder.add(params, 'siteId').name('Site ID');
// events
optionsFolder.add(this.params, 'siteId').name('Site ID');

const translationGui = optionsFolder.addFolder('Translation');
translationGui.add(this.translation, 'x').name('Translation X');
Expand All @@ -96,18 +115,22 @@ export class Image360UI {
rotationGui.add(this.rotation, 'z').name('Rotation Axis Z');
rotationGui.add(this.rotation, 'radians', 0, 2 * Math.PI, 0.001);

optionsFolder.add(params, 'premultipliedRotation').name('Pre-multiplied rotation');
optionsFolder.add(this.params, 'premultipliedRotation').name('Pre-multiplied rotation');

// data models
optionsFolderFdm.add(this.params, 'siteId').name('External ID');
optionsFolderFdm.add(this.params, 'space').name('Space');

this.gui.add(params, 'add').name('Add image set');
this.gui.add(params, 'remove').name('Remove image set');
this.gui.add(this.params, 'add').name('Add image set');
this.gui.add(this.params, 'remove').name('Remove image set');

this.gui.add(this.opacity, 'alpha', 0, 1, 0.01).onChange(() => {
this.entities.forEach(p => (p.image360Visualization.opacity = this.opacity.alpha));
this.viewer.requestRedraw();
});

gui.add(params, 'assetId').name('Asset ID');
gui.add(params, 'findAsset').name('Find asset');
gui.add(this.params, 'assetId').name('Asset ID');
gui.add(this.params, 'findAsset').name('Find asset');

this.gui
.add(this.iconCulling, 'radius', 0, 10000, 1)
Expand All @@ -133,37 +156,37 @@ export class Image360UI {
}
});

this.gui.add(params, 'saveToUrl').name('Save 360 site to URL');
this.gui.add(params, 'removeAll').name('Remove all 360 images');
this.gui.add(this.params, 'saveToUrl').name('Save 360 site to URL');
this.gui.add(this.params, 'removeAll').name('Remove all 360 images');

//restore image 360
if (params.siteId.length > 0) {
this.add360ImageSet();
gui
.add(imageRevisions, 'targetDate')
.name('Revision date (Unix epoch time):')
.onChange(() => {
if (collections.length === 0) return;

const date = imageRevisions.targetDate.length > 0 ? new Date(Number(imageRevisions.targetDate)) : undefined;
collections.forEach(p => (p.targetRevisionDate = date));
if (selectedEntity) viewer.enter360Image(selectedEntity);
});

gui
.add(imageRevisions, 'id')
.name('Current image revision')
.onChange(() => {
if (selectedEntity) {
const revisions = selectedEntity.getRevisions();
const index = Number(imageRevisions.id);
if (index >= 0 && index < revisions.length) {
viewer.enter360Image(selectedEntity, revisions[index]);
}
gui
.add(this.imageRevisions, 'targetDate')
.name('Revision date (Unix epoch time):')
.onChange(() => {
if (this.collections.length === 0) return;

const date =
this.imageRevisions.targetDate.length > 0 ? new Date(Number(this.imageRevisions.targetDate)) : undefined;
this.collections.forEach(p => (p.targetRevisionDate = date));
if (this.selectedEntity) viewer.enter360Image(this.selectedEntity);
});

gui
.add(this.imageRevisions, 'id')
.name('Current image revision')
.onChange(() => {
if (this.selectedEntity) {
const revisions = this.selectedEntity.getRevisions();
const index = Number(this.imageRevisions.id);
if (index >= 0 && index < revisions.length) {
viewer.enter360Image(this.selectedEntity, revisions[index]);
}
});
}
});

gui.add(params, 'remove').name('Remove all 360 images');
//restore image 360
if (this.params.siteId.length > 0) {
this.add360ImageSet();
}
}

Expand All @@ -180,9 +203,28 @@ export class Image360UI {
}

private async add360ImageSet() {
const { params, _collections: collections } = this;
if (this.params.siteId.length === 0) return;

if (params.siteId.length === 0) return;
const collection = await this.addCollection();

collection.setIconsVisibility(!this.iconCulling.hideAll);
collection.on('image360Entered', (entity, _) => {
this.selectedEntity = entity;
});
this.viewer.on('click', event => this.onAnnotationClicked(event));
this._collections.push(collection);
this.entities = this.entities.concat(collection.image360Entities);

this.viewer.requestRedraw();
}

private addCollection(): Promise<Image360Collection> {
if (this.dataSource.type === 'dataModels') {
return this.viewer.add360ImageSet('datamodels', {
image360CollectionExternalId: this.params.siteId,
space: this.params.space
});
}

const rotationMatrix = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(this.rotation.x, this.rotation.y, this.rotation.z),
Expand All @@ -194,23 +236,15 @@ export class Image360UI {
this.translation.z
);
const collectionTransform = translationMatrix.multiply(rotationMatrix);
const collection = await this.viewer.add360ImageSet(
return this.viewer.add360ImageSet(
'events',
{ site_id: params.siteId },
{ site_id: this.params.siteId },
{
collectionTransform,
preMultipliedRotation: params.premultipliedRotation,
preMultipliedRotation: this.params.premultipliedRotation,
annotationFilter: { status: 'all' }
}
);

collection.setIconsVisibility(!this.iconCulling.hideAll);
collection.on('image360Entered', (entity, _) => (this.selectedEntity = entity));
this.viewer.on('click', event => this.onAnnotationClicked(event));
collections.push(collection);
this.entities = this.entities.concat(collection.image360Entities);

this.viewer.requestRedraw();
}

private async set360IconCullingRestrictions() {
Expand All @@ -221,7 +255,7 @@ export class Image360UI {
}

private async removeAll360Images() {
await this.viewer.remove360Images(...this.entities);
this._collections.forEach(p => this.viewer.remove360ImageSet(p));
this.entities = [];
this._collections = [];
}
Expand All @@ -242,6 +276,9 @@ export class Image360UI {

const url = new URL(window.location.href);
url.searchParams.set('siteId', params.siteId);
if (params.space.length > 0) {
url.searchParams.set('space', params.space);
}
window.history.replaceState(null, document.title, url.toString());
}

Expand Down Expand Up @@ -274,3 +311,9 @@ function getSiteIdFromUrl() {
const siteId = url.searchParams.get('siteId');
return siteId;
}

function getSpaceFromUrl() {
const url = new URL(window.location.href);
const siteId = url.searchParams.get('space');
return siteId;
}
6 changes: 5 additions & 1 deletion viewer/api-entry-points/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ export {
CDF_TO_VIEWER_TRANSFORMATION
} from '../packages/utilities';

export { PointCloudObjectMetadata, ImageAssetLinkAnnotationInfo } from '../packages/data-providers';
export {
PointCloudObjectMetadata,
ImageAssetLinkAnnotationInfo,
Image360DataModelIdentifier
} from '../packages/data-providers';

export { CogniteCadModel, BoundingBoxClipper, GeometryFilter, WellKnownUnit } from '../packages/cad-model';

Expand Down
11 changes: 6 additions & 5 deletions viewer/packages/360-images/visual-tests/Image360.VisualTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import * as THREE from 'three';

import { Cdf360ImageEventProvider, Local360ImageProvider } from '@reveal/data-providers';
import { Cdf360EventDescriptorProvider, Cdf360ImageProvider, Local360ImageProvider } from '@reveal/data-providers';
import { StreamingTestFixtureComponents } from '../../../visual-tests/test-fixtures/StreamingVisualTestFixture';
import { StreamingVisualTestFixture } from '../../../visual-tests';
import { Image360Facade } from '../src/Image360Facade';
Expand Down Expand Up @@ -50,7 +50,7 @@ export default class Image360VisualTestFixture extends StreamingVisualTestFixtur

this.setupGUI(entities);

this.setupMouseMoveEventHandler(renderer, entities, facade, camera);
this.setupMouseMoveEventHandler(renderer, facade, camera);

this.setupMouseClickEventHandler(renderer, facade, camera, cameraControls);
}
Expand Down Expand Up @@ -172,7 +172,6 @@ export default class Image360VisualTestFixture extends StreamingVisualTestFixtur

private setupMouseMoveEventHandler(
renderer: THREE.WebGLRenderer,
entities: Image360Entity[],
facade: CdfImage360Facade | LocalImage360Facade,
camera: THREE.PerspectiveCamera
) {
Expand Down Expand Up @@ -249,7 +248,8 @@ export default class Image360VisualTestFixture extends StreamingVisualTestFixtur
}>;
entities: Image360Entity[];
}> {
const cdf360ImageProvider = new Cdf360ImageEventProvider(cogniteClient);
const cdf360EventDescriptorProvider = new Cdf360EventDescriptorProvider(cogniteClient);
const cdf360ImageProvider = new Cdf360ImageProvider(cogniteClient, cdf360EventDescriptorProvider);
const image360Factory = new Image360CollectionFactory(cdf360ImageProvider, sceneHandler, onBeforeRender, device);
const image360Facade = new Image360Facade(image360Factory);
const rotation = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), 0.1);
Expand All @@ -275,7 +275,8 @@ export default class Image360VisualTestFixture extends StreamingVisualTestFixtur
}>;
entities: Image360Entity[];
}> {
const cdf360ImageProvider = new Cdf360ImageEventProvider(cogniteClient);
const cdf360EventDescriptorProvider = new Cdf360EventDescriptorProvider(cogniteClient);
const cdf360ImageProvider = new Cdf360ImageProvider(cogniteClient, cdf360EventDescriptorProvider);
const image360Factory = new Image360CollectionFactory(cdf360ImageProvider, sceneHandler, onBeforeRender, device);
const image360Facade = new Image360Facade(image360Factory);

Expand Down
52 changes: 40 additions & 12 deletions viewer/packages/api/src/api-helpers/Image360ApiHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
Image360AnnotationIntersection,
Image360AnnotationFilterOptions
} from '@reveal/360-images';
import { Cdf360ImageEventProvider } from '@reveal/data-providers';
import {
Cdf360CombinedDescriptorProvider,
Cdf360DataModelsDescriptorProvider,
Cdf360EventDescriptorProvider,
Cdf360ImageProvider,
Image360DataModelIdentifier
} from '@reveal/data-providers';
import {
BeforeSceneRenderedDelegate,
determineCurrentDevice,
Expand All @@ -32,7 +38,7 @@ import { MetricsLogger } from '@reveal/metrics';
import debounce from 'lodash/debounce';

export class Image360ApiHelper {
private readonly _image360Facade: Image360Facade<Metadata>;
private readonly _image360Facade: Image360Facade<Metadata | Image360DataModelIdentifier>;
private readonly _domElement: HTMLElement;
private _transitionInProgress: boolean = false;
private readonly _raycaster = new THREE.Raycaster();
Expand Down Expand Up @@ -77,7 +83,13 @@ export class Image360ApiHelper {
onBeforeSceneRendered: EventTrigger<BeforeSceneRenderedDelegate>,
iconsOptions?: IconsOptions
) {
const image360DataProvider = new Cdf360ImageEventProvider(cogniteClient);
const image360EventDescriptorProvider = new Cdf360EventDescriptorProvider(cogniteClient);
const image360DataModelsDescriptorProvider = new Cdf360DataModelsDescriptorProvider(cogniteClient);
const combinedDescriptorProvider = new Cdf360CombinedDescriptorProvider(
image360DataModelsDescriptorProvider,
image360EventDescriptorProvider
);
const image360DataProvider = new Cdf360ImageProvider(cogniteClient, combinedDescriptorProvider);
const device = determineCurrentDevice();
const image360EntityFactory = new Image360CollectionFactory(
image360DataProvider,
Expand Down Expand Up @@ -136,28 +148,44 @@ export class Image360ApiHelper {
}

public async add360ImageSet(
eventFilter: Metadata,
collectionIdentifier: Metadata | Image360DataModelIdentifier,
collectionTransform: THREE.Matrix4,
preMultipliedRotation: boolean,
annotationOptions?: Image360AnnotationFilterOptions
): Promise<Image360Collection> {
const id: string | undefined = eventFilter.site_id;
if (id === undefined) {
throw new Error('Image set filter must contain site_id');
}
if (this._image360Facade.collections.map(collection => collection.id).includes(id)) {
throw new Error(`Image set with id=${id} has already been added`);
}
validateIds(this._image360Facade);

const imageCollection = await this._image360Facade.create(
eventFilter,
collectionIdentifier,
annotationOptions,
collectionTransform,
preMultipliedRotation
);

this._needsRedraw = true;
return imageCollection;

function validateIds(image360Facade: Image360Facade<Metadata | Image360DataModelIdentifier>) {
if (!Cdf360CombinedDescriptorProvider.isFdmIdentifier(collectionIdentifier)) {
const id: string | undefined = collectionIdentifier.site_id;
if (id === undefined) {
throw new Error('Image set filter must contain site_id');
}
if (image360Facade.collections.map(collection => collection.id).includes(id)) {
throw new Error(`Image set with id=${id} has already been added`);
}
} else {
if (
image360Facade.collections
.map(collection => collection.id)
.includes(collectionIdentifier.image360CollectionExternalId)
) {
throw new Error(
`Image set with id=${collectionIdentifier.image360CollectionExternalId} has already been added`
);
}
}
}
}

public getImageCollections(): Image360Collection[] {
Expand Down
Loading

0 comments on commit 74eb8a9

Please sign in to comment.