From e47a5e5d52bc875796ea244cd27aa8e645e1ea5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= <70905152+haakonflatval-cognite@users.noreply.github.com> Date: Fri, 18 Oct 2024 08:31:27 +0200 Subject: [PATCH] refactor(react-components): rename observations -> points of interest and factor out FDM provider (#4810) * chore: generalize PoI storage * chore: Add generics EVERYWHERE * chore: add generic specifier at top level * chore: observation => point of interest * chore: move util function a bit more * chore: rename provider+file * revert: revert unnecessary change * chore: rename variables/symbol --- .../concrete/config/StoryBookConfig.ts | 5 +- .../observations/ObservationsCache.ts | 52 ------ .../observations/ObservationsCommand.ts | 16 -- .../observations/ObservationsDomainObject.ts | 159 ----------------- .../concrete/observations/ObservationsTool.ts | 105 ----------- .../concrete/observations/network.ts | 100 ----------- .../concrete/observations/types.ts | 65 ------- .../CreatePointsOfInterestCommand.ts} | 6 +- .../DeletePointsOfInterestCommand.ts} | 20 +-- .../pointsOfInterest/PointsOfInterestCache.ts | 43 +++++ .../PointsOfInterestCommand.ts | 16 ++ .../PointsOfInterestDomainObject.ts | 165 ++++++++++++++++++ .../PointsOfInterestProvider.ts | 12 ++ .../pointsOfInterest/PointsOfInterestTool.ts | 111 ++++++++++++ .../PointsOfInterestView.ts} | 51 +++--- .../SavePointsOfInterestCommand.ts} | 21 +-- .../color.ts | 12 +- .../fdm/PointsOfInterestFdmProvider.ts | 29 +++ .../concrete/pointsOfInterest/fdm/network.ts | 107 ++++++++++++ .../concrete/pointsOfInterest/fdm/view.ts | 11 ++ .../models.ts | 26 +-- .../concrete/pointsOfInterest/types.ts | 64 +++++++ .../components/Architecture/RevealButtons.tsx | 6 +- .../core-dm-provider/CoreDm3dDataProvider.ts | 2 +- .../getCadConnectionsForRevisions.ts | 2 +- .../core-dm-provider/getDMSRevision.ts | 2 +- .../getFdmConnectionsForNodes.ts | 2 +- .../restrictToDmsId.ts | 2 +- 28 files changed, 633 insertions(+), 579 deletions(-) delete mode 100644 react-components/src/architecture/concrete/observations/ObservationsCache.ts delete mode 100644 react-components/src/architecture/concrete/observations/ObservationsCommand.ts delete mode 100644 react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts delete mode 100644 react-components/src/architecture/concrete/observations/ObservationsTool.ts delete mode 100644 react-components/src/architecture/concrete/observations/network.ts delete mode 100644 react-components/src/architecture/concrete/observations/types.ts rename react-components/src/architecture/concrete/{observations/CreateObservationCommand.ts => pointsOfInterest/CreatePointsOfInterestCommand.ts} (65%) rename react-components/src/architecture/concrete/{observations/DeleteObservationCommand.ts => pointsOfInterest/DeletePointsOfInterestCommand.ts} (52%) create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCache.ts create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCommand.ts create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestDomainObject.ts create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestProvider.ts create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts rename react-components/src/architecture/concrete/{observations/ObservationsView.ts => pointsOfInterest/PointsOfInterestView.ts} (70%) rename react-components/src/architecture/concrete/{observations/SaveObservationsCommand.ts => pointsOfInterest/SavePointsOfInterestCommand.ts} (63%) rename react-components/src/architecture/concrete/{observations => pointsOfInterest}/color.ts (70%) create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/fdm/PointsOfInterestFdmProvider.ts create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/fdm/network.ts create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/fdm/view.ts rename react-components/src/architecture/concrete/{observations => pointsOfInterest}/models.ts (72%) create mode 100644 react-components/src/architecture/concrete/pointsOfInterest/types.ts rename react-components/src/{data-providers/core-dm-provider => utilities}/restrictToDmsId.ts (75%) diff --git a/react-components/src/architecture/concrete/config/StoryBookConfig.ts b/react-components/src/architecture/concrete/config/StoryBookConfig.ts index 325937e19fc..cc8ddd0eff1 100644 --- a/react-components/src/architecture/concrete/config/StoryBookConfig.ts +++ b/react-components/src/architecture/concrete/config/StoryBookConfig.ts @@ -17,7 +17,6 @@ import { ToggleMetricUnitsCommand } from '../../base/concreteCommands/ToggleMetr import { MeasurementTool } from '../measurements/MeasurementTool'; import { ClipTool } from '../clipping/ClipTool'; import { KeyboardSpeedCommand } from '../../base/concreteCommands/KeyboardSpeedCommand'; -import { ObservationsTool } from '../observations/ObservationsTool'; import { SettingsCommand } from '../../base/concreteCommands/SettingsCommand'; import { MockSettingsCommand } from '../../base/commands/mocks/MockSettingsCommand'; import { MockFilterCommand } from '../../base/commands/mocks/MockFilterCommand'; @@ -28,6 +27,8 @@ import { AnnotationsCreateTool } from '../annotations/commands/AnnotationsCreate import { AnnotationsShowCommand } from '../annotations/commands/AnnotationsShowCommand'; import { AnnotationsShowOnTopCommand } from '../annotations/commands/AnnotationsShowOnTopCommand'; import { AnnotationsSelectTool } from '../annotations/commands/AnnotationsSelectTool'; +import { type DmsUniqueIdentifier } from '../../../data-providers'; +import { PointsOfInterestTool } from '../pointsOfInterest/PointsOfInterestTool'; export class StoryBookConfig extends BaseRevealConfig { // ================================================== @@ -55,7 +56,7 @@ export class StoryBookConfig extends BaseRevealConfig { undefined, new MeasurementTool(), new ClipTool(), - new ObservationsTool(), + new PointsOfInterestTool(), undefined, new AnnotationsSelectTool(), new AnnotationsCreateTool(), diff --git a/react-components/src/architecture/concrete/observations/ObservationsCache.ts b/react-components/src/architecture/concrete/observations/ObservationsCache.ts deleted file mode 100644 index bc9cca6fe0d..00000000000 --- a/react-components/src/architecture/concrete/observations/ObservationsCache.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { type FdmSDK } from '../../../data-providers/FdmSDK'; -import { type ObservationFdmNode, type ObservationProperties } from './models'; - -import { type Observation } from './types'; -import { - createObservationInstances, - deleteObservationInstances, - fetchObservations -} from './network'; -import { isDefined } from '../../../utilities/isDefined'; - -/** - * A cache that takes care of loading the observations, but also buffers changes to the overlays - * list when e.g. adding or removing observations - */ -export class ObservationsCache { - private readonly _loadedPromise: Promise; - - constructor(fdmSdk: FdmSDK) { - this._loadedPromise = fetchObservations(fdmSdk); - } - - public async getFinishedOriginalLoadingPromise(): Promise { - return await this._loadedPromise; - } - - public async deleteObservations(fdmSdk: FdmSDK, observations: Observation[]): Promise { - if (observations.length === 0) { - return; - } - - const observationData = observations - .map((observation) => observation.fdmMetadata) - .filter(isDefined); - - await deleteObservationInstances(fdmSdk, observationData); - } - - public async saveObservations( - fdmSdk: FdmSDK, - observations: ObservationProperties[] - ): Promise { - if (observations.length === 0) { - return []; - } - - return await createObservationInstances(fdmSdk, observations); - } -} diff --git a/react-components/src/architecture/concrete/observations/ObservationsCommand.ts b/react-components/src/architecture/concrete/observations/ObservationsCommand.ts deleted file mode 100644 index 8b2322cfa57..00000000000 --- a/react-components/src/architecture/concrete/observations/ObservationsCommand.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; -import { ObservationsDomainObject } from './ObservationsDomainObject'; -import { ObservationsTool } from './ObservationsTool'; - -export abstract class ObservationsCommand extends RenderTargetCommand { - protected getTool(): ObservationsTool | undefined { - return this.getActiveTool(ObservationsTool); - } - - protected getObservationsDomainObject(): ObservationsDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); - } -} diff --git a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts b/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts deleted file mode 100644 index 1dd3c9f96e9..00000000000 --- a/react-components/src/architecture/concrete/observations/ObservationsDomainObject.ts +++ /dev/null @@ -1,159 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; -import { type ThreeView } from '../../base/views/ThreeView'; -import { ObservationsView } from './ObservationsView'; -import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { type FdmSDK } from '../../../data-providers/FdmSDK'; -import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { ObservationsCache } from './ObservationsCache'; -import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; -import { type Observation, ObservationStatus } from './types'; -import { partition, remove } from 'lodash'; -import { type ObservationProperties } from './models'; -import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; - -export class ObservationsDomainObject extends VisualDomainObject { - private _selectedObservation: Observation | undefined; - private readonly _observationsCache: ObservationsCache; - - private _observations: Observation[] = []; - - constructor(fdmSdk: FdmSDK) { - super(); - - this._observationsCache = new ObservationsCache(fdmSdk); - void this._observationsCache.getFinishedOriginalLoadingPromise().then((observations) => { - this._observations = observations.map((observation) => ({ - fdmMetadata: observation, - properties: observation.properties, - status: ObservationStatus.Default - })); - this.notify(Changes.geometry); - }); - } - - public override get typeName(): TranslateKey { - return { fallback: ObservationsDomainObject.name }; - } - - protected override createThreeView(): ThreeView | undefined { - return new ObservationsView(); - } - - public override get canBeRemoved(): boolean { - return false; - } - - public override get hasPanelInfo(): boolean { - return true; - } - - public override getPanelInfo(): PanelInfo | undefined { - const info = new PanelInfo(); - const header = { fallback: 'Observation' }; - info.setHeader(header); - - if (this._selectedObservation !== undefined) { - const properties = this._selectedObservation.properties; - info.add({ fallback: 'X', value: properties.positionX, quantity: Quantity.Length }); - info.add({ fallback: 'Y', value: properties.positionY, quantity: Quantity.Length }); - info.add({ fallback: 'Z', value: properties.positionZ, quantity: Quantity.Length }); - } - return info; - } - - public addPendingObservation(observationData: ObservationProperties): Observation { - const newObservation = { - properties: observationData, - status: ObservationStatus.PendingCreation - }; - - this._observations.push(newObservation); - - this.notify(Changes.geometry); - - return newObservation; - } - - public removeObservation(observation: Observation): void { - if (observation.status === ObservationStatus.PendingCreation) { - remove(this._observations, observation); - } else if (this._observations.includes(observation)) { - observation.status = ObservationStatus.PendingDeletion; - } - - this.notify(Changes.geometry); - } - - public get observations(): Observation[] { - return this._observations; - } - - public get selectedObservation(): Observation | undefined { - return this._selectedObservation; - } - - public async save(): Promise { - const fdmSdk = this.rootDomainObject?.fdmSdk; - if (fdmSdk === undefined) { - return fdmSdk; - } - - if ( - this._selectedObservation !== undefined && - (this._selectedObservation.status === ObservationStatus.PendingCreation || - this._selectedObservation.status === ObservationStatus.PendingDeletion) - ) { - this.setSelectedObservation(undefined); - } - - const [toRemove, notToRemove] = partition( - this._observations, - (observation) => observation.status === ObservationStatus.PendingDeletion - ); - - const deletePromise = this._observationsCache.deleteObservations(fdmSdk, toRemove); - - const observationsToCreate = this._observations.filter( - (obs) => obs.status === ObservationStatus.PendingCreation - ); - const newObservations = await this._observationsCache.saveObservations( - fdmSdk, - observationsToCreate.map((obs) => obs.properties) - ); - - this._observations = notToRemove - .filter((observation) => observation.status === ObservationStatus.Default) - .concat( - newObservations.map((observation) => ({ - status: ObservationStatus.Default, - fdmMetadata: observation, - properties: observation.properties - })) - ); - - await deletePromise; - - this.notify(Changes.geometry); - } - - public setSelectedObservation(observation: Observation | undefined): void { - this._selectedObservation = observation; - - this.notify(Changes.selected); - } - - public hasPendingObservations(): boolean { - return this._observations.some( - (observation) => observation.status === ObservationStatus.PendingCreation - ); - } - - public hasPendingDeletionObservations(): boolean { - return this._observations.some( - (observations) => observations.status === ObservationStatus.PendingDeletion - ); - } -} diff --git a/react-components/src/architecture/concrete/observations/ObservationsTool.ts b/react-components/src/architecture/concrete/observations/ObservationsTool.ts deleted file mode 100644 index 5ddf800425b..00000000000 --- a/react-components/src/architecture/concrete/observations/ObservationsTool.ts +++ /dev/null @@ -1,105 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { ObservationsDomainObject } from './ObservationsDomainObject'; -import { BaseEditTool } from '../../base/commands/BaseEditTool'; -import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; -import { type BaseCommand } from '../../base/commands/BaseCommand'; -import { CreateObservationCommand } from './CreateObservationCommand'; -import { SaveObservationsCommand } from './SaveObservationsCommand'; -import { DeleteObservationCommand } from './DeleteObservationCommand'; -import { createEmptyObservationProperties, isObservationIntersection } from './types'; -import { type IconName } from '../../base/utilities/IconName'; - -export class ObservationsTool extends BaseEditTool { - private _isCreating: boolean = false; - - protected override canBeSelected(domainObject: VisualDomainObject): boolean { - return domainObject instanceof ObservationsDomainObject; - } - - public override get icon(): IconName { - return 'Location'; - } - - public override get tooltip(): TranslateKey { - return { fallback: 'Show and edit observations' }; - } - - public override getToolbar(): Array { - return [ - new CreateObservationCommand(), - new DeleteObservationCommand(), - new SaveObservationsCommand() - ]; - } - - public override onActivate(): void { - super.onActivate(); - let domainObject = this.getObservationsDomainObject(); - if (domainObject === undefined) { - domainObject = new ObservationsDomainObject(this.rootDomainObject.fdmSdk); - this.renderTarget.rootDomainObject.addChildInteractive(domainObject); - } - domainObject.setVisibleInteractive(true, this.renderTarget); - } - - public override onDeactivate(): void { - super.onDeactivate(); - const domainObject = this.getObservationsDomainObject(); - domainObject?.setSelectedObservation(undefined); - } - - public override async onClick(event: PointerEvent): Promise { - if (this._isCreating) { - await this.createPendingObservation(event); - return; - } - await this.selectOverlayFromClick(event); - } - - public getObservationsDomainObject(): ObservationsDomainObject | undefined { - return this.rootDomainObject.getDescendantByType(ObservationsDomainObject); - } - - public get isCreating(): boolean { - return this._isCreating; - } - - public setIsCreating(value: boolean): void { - this._isCreating = value; - if (value) { - this.renderTarget.setCrosshairCursor(); - } else { - this.renderTarget.setNavigateCursor(); - } - } - - private async selectOverlayFromClick(event: PointerEvent): Promise { - const intersection = await this.getIntersection(event); - - if (intersection === undefined || !isObservationIntersection(intersection)) { - await super.onClick(event); - return; - } - - intersection.domainObject.setSelectedObservation(intersection.userData); - } - - private async createPendingObservation(event: PointerEvent): Promise { - const intersection = await this.getIntersection(event); - - if (intersection === undefined) { - await super.onClick(event); - return; - } - const domainObject = this.getObservationsDomainObject(); - const pendingOverlay = domainObject?.addPendingObservation( - createEmptyObservationProperties(intersection.point) - ); - domainObject?.setSelectedObservation(pendingOverlay); - - this.setIsCreating(false); - } -} diff --git a/react-components/src/architecture/concrete/observations/network.ts b/react-components/src/architecture/concrete/observations/network.ts deleted file mode 100644 index 73095878e49..00000000000 --- a/react-components/src/architecture/concrete/observations/network.ts +++ /dev/null @@ -1,100 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { chunk } from 'lodash'; -import { - type CreateInstanceItem, - type DmsUniqueIdentifier, - type FdmSDK -} from '../../../data-providers/FdmSDK'; -import { type ObservationFdmNode, OBSERVATION_SOURCE, type ObservationProperties } from './models'; - -import { v4 as uuid } from 'uuid'; - -export async function fetchObservations(fdmSdk: FdmSDK): Promise { - const observationResult = await fdmSdk.filterAllInstances( - undefined, - 'node', - OBSERVATION_SOURCE - ); - - return observationResult.instances; -} - -export async function createObservationInstances( - fdmSdk: FdmSDK, - observationOverlays: ObservationProperties[] -): Promise { - const chunks = chunk(observationOverlays, 100); - const resultPromises = chunks.map(async (chunk) => { - const payloads = chunk.map(createObservationInstancePayload); - const instanceResults = await fdmSdk.createInstance(payloads); - return instanceResults.items; - }); - - const createResults = (await Promise.all(resultPromises)).flat(); - - return await fetchObservationsWithIds(fdmSdk, createResults); -} - -async function fetchObservationsWithIds( - fdmSdk: FdmSDK, - identifiers: DmsUniqueIdentifier[] -): Promise { - return ( - await fdmSdk.filterInstances( - { - and: [ - { - in: { - property: ['node', 'externalId'], - values: identifiers.map((identifier) => identifier.externalId) - } - }, - { - in: { - property: ['node', 'space'], - values: identifiers.map((identifier) => identifier.space) - } - } - ] - }, - 'node', - OBSERVATION_SOURCE - ) - ).instances.map((observation) => observation); -} - -function createObservationInstancePayload( - observation: ObservationProperties -): CreateInstanceItem { - const id = uuid(); - return { - instanceType: 'node' as const, - externalId: uuid(), - space: 'observations', - sources: [ - { - source: OBSERVATION_SOURCE, - properties: { - ...observation, - type: 'simple', - sourceId: id - } - } - ] - }; -} - -export async function deleteObservationInstances( - fdmSdk: FdmSDK, - observations: ObservationFdmNode[] -): Promise { - await fdmSdk.deleteInstances( - observations.map((observation) => ({ - instanceType: 'node', - externalId: observation.externalId, - space: observation.space - })) - ); -} diff --git a/react-components/src/architecture/concrete/observations/types.ts b/react-components/src/architecture/concrete/observations/types.ts deleted file mode 100644 index 92ea743dda6..00000000000 --- a/react-components/src/architecture/concrete/observations/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*! - * Copyright 2024 Cognite AS - */ -import { - type AnyIntersection, - CDF_TO_VIEWER_TRANSFORMATION, - type ICustomObject -} from '@cognite/reveal'; -import { type ObservationProperties } from './models'; -import { type Vector3 } from 'three'; -import { type FdmNode } from '../../../data-providers/FdmSDK'; -import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; -import { type ObservationsDomainObject } from './ObservationsDomainObject'; - -export enum ObservationStatus { - Default, - PendingDeletion, - PendingCreation -} - -export type Observation = { - properties: ObservationProperties; - fdmMetadata?: FdmNode; - status: ObservationStatus; -}; - -export function createEmptyObservationProperties(point: Vector3): ObservationProperties { - const cdfPosition = point.clone().applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); - return { positionX: cdfPosition.x, positionY: cdfPosition.y, positionZ: cdfPosition.z }; -} - -const observationMarker = Symbol('observationSymbol'); - -export type ObservationIntersection = Omit< - DomainObjectIntersection, - 'userData' | 'domainObject' -> & { - marker: typeof observationMarker; - domainObject: ObservationsDomainObject; - userData: Observation; -}; - -export function createObservationIntersection( - point: Vector3, - distanceToCamera: number, - customObject: ICustomObject, - domainObject: ObservationsDomainObject, - overlay: Observation -): ObservationIntersection { - return { - type: 'customObject', - marker: observationMarker, - point, - distanceToCamera, - customObject, - domainObject, - userData: overlay - }; -} - -export function isObservationIntersection( - objectIntersection: AnyIntersection -): objectIntersection is ObservationIntersection { - return (objectIntersection as ObservationIntersection).marker === observationMarker; -} diff --git a/react-components/src/architecture/concrete/observations/CreateObservationCommand.ts b/react-components/src/architecture/concrete/pointsOfInterest/CreatePointsOfInterestCommand.ts similarity index 65% rename from react-components/src/architecture/concrete/observations/CreateObservationCommand.ts rename to react-components/src/architecture/concrete/pointsOfInterest/CreatePointsOfInterestCommand.ts index 3691638ba56..bc147bb02dd 100644 --- a/react-components/src/architecture/concrete/observations/CreateObservationCommand.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/CreatePointsOfInterestCommand.ts @@ -3,15 +3,15 @@ */ import { type IconName } from '../../base/utilities/IconName'; import { type TranslateKey } from '../../base/utilities/TranslateKey'; -import { ObservationsCommand } from './ObservationsCommand'; +import { PointsOfInterestCommand } from './PointsOfInterestCommand'; -export class CreateObservationCommand extends ObservationsCommand { +export class CreatePointsOfInterestCommand extends PointsOfInterestCommand { public override get icon(): IconName { return 'Plus'; } public override get tooltip(): TranslateKey { - return { key: 'ADD_OBSERVATION', fallback: 'Add observation. Click at a point' }; + return { key: 'ADD_OBSERVATION', fallback: 'Add point of interest. Click at a point' }; } protected override invokeCore(): boolean { diff --git a/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts b/react-components/src/architecture/concrete/pointsOfInterest/DeletePointsOfInterestCommand.ts similarity index 52% rename from react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts rename to react-components/src/architecture/concrete/pointsOfInterest/DeletePointsOfInterestCommand.ts index 05e37ff3e75..fd3dff5be00 100644 --- a/react-components/src/architecture/concrete/observations/DeleteObservationCommand.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/DeletePointsOfInterestCommand.ts @@ -3,16 +3,16 @@ */ import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { type ButtonType } from '../../../components/Architecture/types'; -import { ObservationsCommand } from './ObservationsCommand'; +import { PointsOfInterestCommand } from './PointsOfInterestCommand'; import { type IconName } from '../../base/utilities/IconName'; -export class DeleteObservationCommand extends ObservationsCommand { +export class DeletePointsOfInterestCommand extends PointsOfInterestCommand { public override get icon(): IconName { return 'Delete'; } public override get tooltip(): TranslateKey { - return { fallback: 'Delete observation' }; + return { fallback: 'Delete point of interest' }; } public override get buttonType(): ButtonType { @@ -24,20 +24,20 @@ export class DeleteObservationCommand extends ObservationsCommand { } public override get isEnabled(): boolean { - const observation = this.getObservationsDomainObject(); + const poi = this.getPointsOfInterestDomainObject(); - return observation?.selectedObservation !== undefined; + return poi?.selectedPointsOfInterest !== undefined; } protected override invokeCore(): boolean { - const observations = this.getObservationsDomainObject(); - const selectedOverlay = observations?.selectedObservation; - if (observations === undefined || selectedOverlay === undefined) { + const pois = this.getPointsOfInterestDomainObject(); + const selectedOverlay = pois?.selectedPointsOfInterest; + if (pois === undefined || selectedOverlay === undefined) { return false; } - observations.removeObservation(selectedOverlay); - observations.setSelectedObservation(undefined); + pois.removePointsOfInterest(selectedOverlay); + pois.setSelectedPointsOfInterest(undefined); return true; } diff --git a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCache.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCache.ts new file mode 100644 index 00000000000..1175a58f83b --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCache.ts @@ -0,0 +1,43 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type PointsOfInterestInstance, type PointsOfInterestProperties } from './models'; +import { type PointsOfInterestProvider } from './PointsOfInterestProvider'; + +/** + * A cache that takes care of loading the pois, but also buffers changes to the overlays + * list when e.g. adding or removing pois + */ +export class PointsOfInterestCache { + private readonly _loadedPromise: Promise>>; + private readonly _poiProvider: PointsOfInterestProvider; + + constructor(poiProvider: PointsOfInterestProvider) { + this._poiProvider = poiProvider; + this._loadedPromise = poiProvider.fetchAllPointsOfInterest(); + } + + public async getFinishedOriginalLoadingPromise(): Promise< + Array> + > { + return await this._loadedPromise; + } + + public async deletePointsOfInterest(poiIds: PoIId[]): Promise { + if (poiIds.length === 0) { + return; + } + + await this._poiProvider.deletePointsOfInterest(poiIds); + } + + public async savePointsOfInterest( + pois: PointsOfInterestProperties[] + ): Promise>> { + if (pois.length === 0) { + return []; + } + + return await this._poiProvider.createPointsOfInterest(pois); + } +} diff --git a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCommand.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCommand.ts new file mode 100644 index 00000000000..bbf3aa2915a --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestCommand.ts @@ -0,0 +1,16 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { RenderTargetCommand } from '../../base/commands/RenderTargetCommand'; +import { PointsOfInterestDomainObject } from './PointsOfInterestDomainObject'; +import { PointsOfInterestTool } from './PointsOfInterestTool'; + +export abstract class PointsOfInterestCommand extends RenderTargetCommand { + protected getTool(): PointsOfInterestTool | undefined { + return this.getActiveTool(PointsOfInterestTool); + } + + protected getPointsOfInterestDomainObject(): PointsOfInterestDomainObject | undefined { + return this.rootDomainObject.getDescendantByType(PointsOfInterestDomainObject); + } +} diff --git a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestDomainObject.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestDomainObject.ts new file mode 100644 index 00000000000..c7d9d654efb --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestDomainObject.ts @@ -0,0 +1,165 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; +import { type ThreeView } from '../../base/views/ThreeView'; +import { PointsOfInterestView } from './PointsOfInterestView'; +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { Changes } from '../../base/domainObjectsHelpers/Changes'; +import { PointsOfInterestCache } from './PointsOfInterestCache'; +import { PanelInfo } from '../../base/domainObjectsHelpers/PanelInfo'; +import { type PointsOfInterest, PointsOfInterestStatus } from './types'; +import { partition, remove } from 'lodash'; +import { type PointsOfInterestProperties } from './models'; +import { Quantity } from '../../base/domainObjectsHelpers/Quantity'; +import { type PointsOfInterestProvider } from './PointsOfInterestProvider'; +import { isDefined } from '../../../utilities/isDefined'; + +export class PointsOfInterestDomainObject extends VisualDomainObject { + private _selectedPointsOfInterest: PointsOfInterest | undefined; + private readonly _poisCache: PointsOfInterestCache; + + private _pointsOfInterest: Array> = []; + + constructor(poiProvider: PointsOfInterestProvider) { + super(); + + this._poisCache = new PointsOfInterestCache(poiProvider); + void this._poisCache.getFinishedOriginalLoadingPromise().then((pois) => { + this._pointsOfInterest = pois.map((poi) => ({ + id: poi.id, + properties: poi.properties, + status: PointsOfInterestStatus.Default + })); + this.notify(Changes.geometry); + }); + } + + public override get typeName(): TranslateKey { + return { fallback: PointsOfInterestDomainObject.name }; + } + + protected override createThreeView(): + | ThreeView> + | undefined { + return new PointsOfInterestView(); + } + + public override get canBeRemoved(): boolean { + return false; + } + + public override get hasPanelInfo(): boolean { + return true; + } + + public override getPanelInfo(): PanelInfo | undefined { + const info = new PanelInfo(); + const header = { fallback: 'PointsOfInterest' }; + info.setHeader(header); + + if (this._selectedPointsOfInterest !== undefined) { + const properties = this._selectedPointsOfInterest.properties; + info.add({ fallback: 'X', value: properties.positionX, quantity: Quantity.Length }); + info.add({ fallback: 'Y', value: properties.positionY, quantity: Quantity.Length }); + info.add({ fallback: 'Z', value: properties.positionZ, quantity: Quantity.Length }); + } + return info; + } + + public addPendingPointsOfInterest( + poiData: PointsOfInterestProperties + ): PointsOfInterest { + const newPointsOfInterest = { + properties: poiData, + status: PointsOfInterestStatus.PendingCreation + }; + + this._pointsOfInterest.push(newPointsOfInterest); + + this.notify(Changes.geometry); + + return newPointsOfInterest; + } + + public removePointsOfInterest(poiToDelete: PointsOfInterest): void { + if (poiToDelete.status === PointsOfInterestStatus.PendingCreation) { + remove(this._pointsOfInterest, (poi) => poiToDelete === poi); + } else if (this._pointsOfInterest.includes(poiToDelete)) { + poiToDelete.status = PointsOfInterestStatus.PendingDeletion; + } + + this.notify(Changes.geometry); + } + + public get pois(): Array> { + return this._pointsOfInterest; + } + + public get selectedPointsOfInterest(): PointsOfInterest | undefined { + return this._selectedPointsOfInterest; + } + + public async save(): Promise { + const fdmSdk = this.rootDomainObject?.fdmSdk; + if (fdmSdk === undefined) { + return fdmSdk; + } + + if ( + this._selectedPointsOfInterest !== undefined && + (this._selectedPointsOfInterest.status === PointsOfInterestStatus.PendingCreation || + this._selectedPointsOfInterest.status === PointsOfInterestStatus.PendingDeletion) + ) { + this.setSelectedPointsOfInterest(undefined); + } + + const [toRemove, notToRemove] = partition( + this._pointsOfInterest, + (poi) => poi.status === PointsOfInterestStatus.PendingDeletion + ); + + const deletePromise = this._poisCache.deletePointsOfInterest( + toRemove.map((poi) => poi.id).filter(isDefined) + ); + + const poisToCreate = this._pointsOfInterest.filter( + (obs) => obs.status === PointsOfInterestStatus.PendingCreation + ); + const newPointsOfInterest = await this._poisCache.savePointsOfInterest( + poisToCreate.map((obs) => obs.properties) + ); + + this._pointsOfInterest = notToRemove + .filter((poi) => poi.status === PointsOfInterestStatus.Default) + .concat( + newPointsOfInterest.map((poi) => ({ + status: PointsOfInterestStatus.Default, + fdmMetadata: poi, + properties: poi.properties + })) + ); + + await deletePromise; + + this.notify(Changes.geometry); + } + + public setSelectedPointsOfInterest(poi: PointsOfInterest | undefined): void { + this._selectedPointsOfInterest = poi; + + this.notify(Changes.selected); + } + + public hasPendingPointsOfInterest(): boolean { + return this._pointsOfInterest.some( + (poi) => poi.status === PointsOfInterestStatus.PendingCreation + ); + } + + public hasPendingDeletionPointsOfInterest(): boolean { + return this._pointsOfInterest.some( + (pois) => pois.status === PointsOfInterestStatus.PendingDeletion + ); + } +} diff --git a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestProvider.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestProvider.ts new file mode 100644 index 00000000000..f34f4d6dc1e --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestProvider.ts @@ -0,0 +1,12 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type PointsOfInterestInstance, type PointsOfInterestProperties } from './models'; + +export type PointsOfInterestProvider = { + createPointsOfInterest: ( + pois: PointsOfInterestProperties[] + ) => Promise>>; + fetchAllPointsOfInterest: () => Promise>>; + deletePointsOfInterest: (poiIds: ID[]) => Promise; +}; diff --git a/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts new file mode 100644 index 00000000000..47993adeae1 --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestTool.ts @@ -0,0 +1,111 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type TranslateKey } from '../../base/utilities/TranslateKey'; +import { PointsOfInterestDomainObject } from './PointsOfInterestDomainObject'; +import { BaseEditTool } from '../../base/commands/BaseEditTool'; +import { type VisualDomainObject } from '../../base/domainObjects/VisualDomainObject'; +import { type BaseCommand } from '../../base/commands/BaseCommand'; +import { CreatePointsOfInterestCommand } from './CreatePointsOfInterestCommand'; +import { SavePointsOfInterestCommand } from './SavePointsOfInterestCommand'; +import { DeletePointsOfInterestCommand } from './DeletePointsOfInterestCommand'; +import { createEmptyPointsOfInterestProperties, isPointsOfInterestIntersection } from './types'; +import { type IconName } from '../../base/utilities/IconName'; +import { PointsOfInterestFdmProvider } from './fdm/PointsOfInterestFdmProvider'; +import { type DmsUniqueIdentifier } from '../../../data-providers'; + +export class PointsOfInterestTool extends BaseEditTool { + private _isCreating: boolean = false; + + protected override canBeSelected(domainObject: VisualDomainObject): boolean { + return domainObject instanceof PointsOfInterestDomainObject; + } + + public override get icon(): IconName { + return 'Location'; + } + + public override get tooltip(): TranslateKey { + return { fallback: 'Show and edit points of interest' }; + } + + public override getToolbar(): Array { + return [ + new CreatePointsOfInterestCommand(), + new DeletePointsOfInterestCommand(), + new SavePointsOfInterestCommand() + ]; + } + + public override onActivate(): void { + super.onActivate(); + let domainObject = this.getPointsOfInterestDomainObject(); + if (domainObject === undefined) { + domainObject = new PointsOfInterestDomainObject( + new PointsOfInterestFdmProvider(this.rootDomainObject.fdmSdk) + ); + this.renderTarget.rootDomainObject.addChildInteractive(domainObject); + } + domainObject.setVisibleInteractive(true, this.renderTarget); + } + + public override onDeactivate(): void { + super.onDeactivate(); + const domainObject = this.getPointsOfInterestDomainObject(); + domainObject?.setSelectedPointsOfInterest(undefined); + } + + public override async onClick(event: PointerEvent): Promise { + if (this._isCreating) { + await this.createPendingPointsOfInterest(event); + return; + } + await this.selectOverlayFromClick(event); + } + + public getPointsOfInterestDomainObject(): + | PointsOfInterestDomainObject + | undefined { + return this.rootDomainObject.getDescendantByType(PointsOfInterestDomainObject); + } + + public get isCreating(): boolean { + return this._isCreating; + } + + public setIsCreating(value: boolean): void { + this._isCreating = value; + if (value) { + this.renderTarget.setCrosshairCursor(); + } else { + this.renderTarget.setNavigateCursor(); + } + } + + private async selectOverlayFromClick(event: PointerEvent): Promise { + const intersection = await this.getIntersection(event); + + if (intersection === undefined || !isPointsOfInterestIntersection(intersection)) { + await super.onClick(event); + return; + } + + intersection.domainObject.setSelectedPointsOfInterest(intersection.userData); + } + + private async createPendingPointsOfInterest(event: PointerEvent): Promise { + const intersection = await this.getIntersection(event); + + if (intersection === undefined) { + await super.onClick(event); + return; + } + const domainObject = this.getPointsOfInterestDomainObject(); + const pendingOverlay = domainObject?.addPendingPointsOfInterest( + createEmptyPointsOfInterestProperties(intersection.point) + ); + domainObject?.setSelectedPointsOfInterest(pendingOverlay); + + this.setIsCreating(false); + } +} diff --git a/react-components/src/architecture/concrete/observations/ObservationsView.ts b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestView.ts similarity index 70% rename from react-components/src/architecture/concrete/observations/ObservationsView.ts rename to react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestView.ts index fc92d424e44..f4faa2b2d9f 100644 --- a/react-components/src/architecture/concrete/observations/ObservationsView.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/PointsOfInterestView.ts @@ -2,7 +2,7 @@ * Copyright 2024 Cognite AS */ import { Box3, Vector3 } from 'three'; -import { type ObservationsDomainObject } from './ObservationsDomainObject'; +import { type PointsOfInterestDomainObject } from './PointsOfInterestDomainObject'; import { GroupThreeView } from '../../base/views/GroupThreeView'; import { CDF_TO_VIEWER_TRANSFORMATION, @@ -14,15 +14,18 @@ import { import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; import { type DomainObjectChange } from '../../base/domainObjectsHelpers/DomainObjectChange'; -import { createObservationIntersection, type Observation } from './types'; +import { createPointsOfInterestIntersection, type PointsOfInterest } from './types'; import { ClosestGeometryFinder } from '../../base/utilities/geometry/ClosestGeometryFinder'; import { getColorFromStatus } from './color'; import { isPointVisibleByPlanes } from '../../base/utilities/geometry/isPointVisibleByPlanes'; -type ObservationCollection = Overlay3DCollection; +type PointsOfInterestCollection = Overlay3DCollection>; -export class ObservationsView extends GroupThreeView { - private readonly _overlayCollection: ObservationCollection = new Overlay3DCollection([]); +export class PointsOfInterestView extends GroupThreeView< + PointsOfInterestDomainObject +> { + private readonly _overlayCollection: PointsOfInterestCollection = + new Overlay3DCollection([]); protected override calculateBoundingBox(): Box3 { const boundingBox = new Box3().makeEmpty(); @@ -35,10 +38,10 @@ export class ObservationsView extends GroupThreeView { } protected override addChildren(): void { - const observations = this.domainObject.observations; + const pois = this.domainObject.pois; - const selectedObservation = this.domainObject.selectedObservation; - const overlayInfos = createObservationOverlays(observations, selectedObservation); + const selectedPointsOfInterest = this.domainObject.selectedPointsOfInterest; + const overlayInfos = createPointsOfInterestOverlays(pois, selectedPointsOfInterest); this._overlayCollection.removeAllOverlays(); this._overlayCollection.addOverlays(overlayInfos); @@ -93,7 +96,7 @@ export class ObservationsView extends GroupThreeView { return undefined; } - const customObjectIntersection = createObservationIntersection( + const customObjectIntersection = createPointsOfInterestIntersection( point, closestFinder.minDistance, this, @@ -105,7 +108,7 @@ export class ObservationsView extends GroupThreeView { return closestFinder.getClosestGeometry(); } - public getOverlays(): ObservationCollection { + public getOverlays(): PointsOfInterestCollection { return this._overlayCollection; } @@ -120,12 +123,12 @@ export class ObservationsView extends GroupThreeView { } private updateColors(): void { - const selectedObservation = this.domainObject.selectedObservation; + const selectedPointsOfInterest = this.domainObject.selectedPointsOfInterest; this._overlayCollection.getOverlays().forEach((overlay) => { const oldColor = overlay.getColor(); const newColor = getColorFromStatus( overlay.getContent().status, - overlay.getContent() === selectedObservation + overlay.getContent() === selectedPointsOfInterest ); if (oldColor.equals(newColor)) { @@ -137,21 +140,21 @@ export class ObservationsView extends GroupThreeView { } } -function createObservationOverlays( - observations: Observation[], - selectedObservation: Observation | undefined -): Array> { - return observations.map((observation) => ({ - position: extractObservationPosition(observation), - content: observation, - color: getColorFromStatus(observation.status, observation === selectedObservation) +function createPointsOfInterestOverlays( + pois: Array>, + selectedPointsOfInterest: PointsOfInterest | undefined +): Array>> { + return pois.map((poi) => ({ + position: extractPointsOfInterestPosition(poi), + content: poi, + color: getColorFromStatus(poi.status, poi === selectedPointsOfInterest) })); } -function extractObservationPosition(observation: Observation): Vector3 { +function extractPointsOfInterestPosition(poi: PointsOfInterest): Vector3 { return new Vector3( - observation.properties.positionX, - observation.properties.positionY, - observation.properties.positionZ + poi.properties.positionX, + poi.properties.positionY, + poi.properties.positionZ ).applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION); } diff --git a/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts b/react-components/src/architecture/concrete/pointsOfInterest/SavePointsOfInterestCommand.ts similarity index 63% rename from react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts rename to react-components/src/architecture/concrete/pointsOfInterest/SavePointsOfInterestCommand.ts index 9dc5942e101..7d87727e149 100644 --- a/react-components/src/architecture/concrete/observations/SaveObservationsCommand.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/SavePointsOfInterestCommand.ts @@ -4,18 +4,18 @@ import { type ButtonType } from '../../../components/Architecture/types'; import { type TranslateKey } from '../../base/utilities/TranslateKey'; import { Changes } from '../../base/domainObjectsHelpers/Changes'; -import { ObservationsCommand } from './ObservationsCommand'; +import { PointsOfInterestCommand } from './PointsOfInterestCommand'; import { type IconName } from '../../base/utilities/IconName'; import { CommandsUpdater } from '../../base/reactUpdaters/CommandsUpdater'; import { makeToast } from '@cognite/cogs-lab'; -export class SaveObservationsCommand extends ObservationsCommand { +export class SavePointsOfInterestCommand extends PointsOfInterestCommand { public override get icon(): IconName { return 'Save'; } public override get tooltip(): TranslateKey { - return { fallback: 'Publish observation changes' }; + return { fallback: 'Publish point of interest changes' }; } public override get buttonType(): ButtonType { @@ -23,11 +23,12 @@ export class SaveObservationsCommand extends ObservationsCommand { } public override get isEnabled(): boolean { - const observation = this.getObservationsDomainObject(); + const domainObject = this.getPointsOfInterestDomainObject(); return ( - observation !== undefined && - (observation.hasPendingObservations() || observation.hasPendingDeletionObservations()) + domainObject !== undefined && + (domainObject.hasPendingPointsOfInterest() || + domainObject.hasPendingDeletionPointsOfInterest()) ); } @@ -37,7 +38,7 @@ export class SaveObservationsCommand extends ObservationsCommand { return false; } - const domainObject = this.getObservationsDomainObject(); + const domainObject = this.getPointsOfInterestDomainObject(); void domainObject ?.save() @@ -46,13 +47,13 @@ export class SaveObservationsCommand extends ObservationsCommand { body: { fallback: 'Successfully published changes' }.fallback, type: 'success' }); - const observation = this.getObservationsDomainObject(); - observation?.notify(Changes.geometry); + const domainObject = this.getPointsOfInterestDomainObject(); + domainObject?.notify(Changes.geometry); CommandsUpdater.update(this.renderTarget); }) .catch((e) => { makeToast({ - body: { fallback: 'Unable to publish observation changes: ' + e }.fallback, + body: { fallback: 'Unable to publish point of interest changes: ' + e }.fallback, type: 'warning' }); throw e; diff --git a/react-components/src/architecture/concrete/observations/color.ts b/react-components/src/architecture/concrete/pointsOfInterest/color.ts similarity index 70% rename from react-components/src/architecture/concrete/observations/color.ts rename to react-components/src/architecture/concrete/pointsOfInterest/color.ts index 5562f1ec183..7ed4892a2d2 100644 --- a/react-components/src/architecture/concrete/observations/color.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/color.ts @@ -3,7 +3,7 @@ */ import { Color } from 'three'; import { assertNever } from '../../../utilities/assertNever'; -import { ObservationStatus } from './types'; +import { PointsOfInterestStatus } from './types'; export const DEFAULT_OVERLAY_COLOR = new Color('#3333AA'); export const PENDING_OVERLAY_COLOR = new Color('#33AA33'); @@ -16,7 +16,7 @@ export function convertToSelectedColor(color: Color): Color { return new Color().setHSL(hsl.h, hsl.s, hsl.l); } -export function getColorFromStatus(status: ObservationStatus, selected: boolean): Color { +export function getColorFromStatus(status: PointsOfInterestStatus, selected: boolean): Color { const baseColor = getBaseColor(status); if (selected) { @@ -25,13 +25,13 @@ export function getColorFromStatus(status: ObservationStatus, selected: boolean) return baseColor; - function getBaseColor(status: ObservationStatus): Color { + function getBaseColor(status: PointsOfInterestStatus): Color { switch (status) { - case ObservationStatus.Default: + case PointsOfInterestStatus.Default: return DEFAULT_OVERLAY_COLOR; - case ObservationStatus.PendingDeletion: + case PointsOfInterestStatus.PendingDeletion: return PENDING_DELETION_OVERLAY_COLOR; - case ObservationStatus.PendingCreation: + case PointsOfInterestStatus.PendingCreation: return PENDING_OVERLAY_COLOR; default: assertNever(status); diff --git a/react-components/src/architecture/concrete/pointsOfInterest/fdm/PointsOfInterestFdmProvider.ts b/react-components/src/architecture/concrete/pointsOfInterest/fdm/PointsOfInterestFdmProvider.ts new file mode 100644 index 00000000000..1bb77f5f778 --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/fdm/PointsOfInterestFdmProvider.ts @@ -0,0 +1,29 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type DmsUniqueIdentifier, type FdmSDK } from '../../../../data-providers/FdmSDK'; +import { type PointsOfInterestInstance, type PointsOfInterestProperties } from '../models'; +import { type PointsOfInterestProvider } from '../PointsOfInterestProvider'; +import { + createPointsOfInterestInstances, + deletePointsOfInterestInstances, + fetchPointsOfInterest +} from './network'; + +export class PointsOfInterestFdmProvider implements PointsOfInterestProvider { + constructor(private readonly _fdmSdk: FdmSDK) {} + + async createPointsOfInterest( + pois: PointsOfInterestProperties[] + ): Promise>> { + return await createPointsOfInterestInstances(this._fdmSdk, pois); + } + + async fetchAllPointsOfInterest(): Promise>> { + return await fetchPointsOfInterest(this._fdmSdk); + } + + async deletePointsOfInterest(ids: DmsUniqueIdentifier[]): Promise { + await deletePointsOfInterestInstances(this._fdmSdk, ids); + } +} diff --git a/react-components/src/architecture/concrete/pointsOfInterest/fdm/network.ts b/react-components/src/architecture/concrete/pointsOfInterest/fdm/network.ts new file mode 100644 index 00000000000..dc7621d8117 --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/fdm/network.ts @@ -0,0 +1,107 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { chunk } from 'lodash'; +import { + type CreateInstanceItem, + type DmsUniqueIdentifier, + type FdmSDK +} from '../../../../data-providers/FdmSDK'; +import { type PointsOfInterestInstance, type PointsOfInterestProperties } from '../models'; + +import { v4 as uuid } from 'uuid'; +import { POI_SOURCE } from './view'; +import { restrictToDmsId } from '../../../../utilities/restrictToDmsId'; + +export async function fetchPointsOfInterest( + fdmSdk: FdmSDK +): Promise>> { + const poiResult = await fdmSdk.filterAllInstances( + undefined, + 'node', + POI_SOURCE + ); + + return poiResult.instances.map((poi) => ({ + id: restrictToDmsId(poi), + properties: poi.properties + })); +} + +export async function createPointsOfInterestInstances( + fdmSdk: FdmSDK, + poiOverlays: PointsOfInterestProperties[] +): Promise>> { + const chunks = chunk(poiOverlays, 100); + const resultPromises = chunks.map(async (chunk) => { + const payloads = chunk.map(createPointsOfInterestInstancePayload); + const instanceResults = await fdmSdk.createInstance(payloads); + return instanceResults.items; + }); + + const createResults = (await Promise.all(resultPromises)).flat(); + + return await fetchPointsOfInterestsWithIds(fdmSdk, createResults); +} + +async function fetchPointsOfInterestsWithIds( + fdmSdk: FdmSDK, + identifiers: DmsUniqueIdentifier[] +): Promise>> { + return ( + await fdmSdk.filterInstances( + { + and: [ + { + in: { + property: ['node', 'externalId'], + values: identifiers.map((identifier) => identifier.externalId) + } + }, + { + in: { + property: ['node', 'space'], + values: identifiers.map((identifier) => identifier.space) + } + } + ] + }, + 'node', + POI_SOURCE + ) + ).instances.map((poi) => ({ + id: restrictToDmsId(poi), + properties: poi.properties + })); +} + +function createPointsOfInterestInstancePayload( + poi: PointsOfInterestProperties +): CreateInstanceItem { + return { + instanceType: 'node' as const, + externalId: uuid(), + space: POI_SOURCE.space, + sources: [ + { + source: POI_SOURCE, + properties: { + ...poi + } + } + ] + }; +} + +export async function deletePointsOfInterestInstances( + fdmSdk: FdmSDK, + ids: DmsUniqueIdentifier[] +): Promise { + await fdmSdk.deleteInstances( + ids.map((id) => ({ + instanceType: 'node', + externalId: id.externalId, + space: id.space + })) + ); +} diff --git a/react-components/src/architecture/concrete/pointsOfInterest/fdm/view.ts b/react-components/src/architecture/concrete/pointsOfInterest/fdm/view.ts new file mode 100644 index 00000000000..22c133ff200 --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/fdm/view.ts @@ -0,0 +1,11 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { type Source } from '../../../../data-providers'; + +export const POI_SOURCE: Source = { + type: 'view', + version: '3c207ca2355dbb', + externalId: 'Observation', + space: 'observations' +}; diff --git a/react-components/src/architecture/concrete/observations/models.ts b/react-components/src/architecture/concrete/pointsOfInterest/models.ts similarity index 72% rename from react-components/src/architecture/concrete/observations/models.ts rename to react-components/src/architecture/concrete/pointsOfInterest/models.ts index 346d16f2676..8a9f32ce595 100644 --- a/react-components/src/architecture/concrete/observations/models.ts +++ b/react-components/src/architecture/concrete/pointsOfInterest/models.ts @@ -1,15 +1,10 @@ /*! * Copyright 2024 Cognite AS */ -import { - type DmsUniqueIdentifier, - type FdmNode, - type Source -} from '../../../data-providers/FdmSDK'; -export type ObservationProperties = { +export type PointsOfInterestProperties = { // "ID as the node appears in the Source system" - sourceId?: string; + /* sourceId?: string; // "Name of the source system node comes from" source?: string; // "Title or name of the node" @@ -38,24 +33,17 @@ export type ObservationProperties = { priority?: string; // "The observation type (Malfunction report, Maintenance request, etc.)" type?: string; - // "3D position" + // "3D position" */ positionX: number; positionY: number; positionZ: number; // "Comments" - comments?: CommentProperties[]; + // comments?: CommentProperties[]; }; -export type CommentProperties = { +/* export type CommentProperties = { createdBy: string; text: string; -}; - -export type ObservationFdmNode = FdmNode; +}; */ -export const OBSERVATION_SOURCE: Source = { - type: 'view', - version: '3c207ca2355dbb', - externalId: 'Observation', - space: 'observations' -}; +export type PointsOfInterestInstance = { id: ID; properties: PointsOfInterestProperties }; diff --git a/react-components/src/architecture/concrete/pointsOfInterest/types.ts b/react-components/src/architecture/concrete/pointsOfInterest/types.ts new file mode 100644 index 00000000000..64de10d134f --- /dev/null +++ b/react-components/src/architecture/concrete/pointsOfInterest/types.ts @@ -0,0 +1,64 @@ +/*! + * Copyright 2024 Cognite AS + */ +import { + type AnyIntersection, + CDF_TO_VIEWER_TRANSFORMATION, + type ICustomObject +} from '@cognite/reveal'; +import { type PointsOfInterestProperties } from './models'; +import { type Vector3 } from 'three'; +import { type DomainObjectIntersection } from '../../base/domainObjectsHelpers/DomainObjectIntersection'; +import { type PointsOfInterestDomainObject } from './PointsOfInterestDomainObject'; + +export enum PointsOfInterestStatus { + Default, + PendingDeletion, + PendingCreation +} + +export type PointsOfInterest = { + properties: PointsOfInterestProperties; + id?: IdType; + status: PointsOfInterestStatus; +}; + +export function createEmptyPointsOfInterestProperties(point: Vector3): PointsOfInterestProperties { + const cdfPosition = point.clone().applyMatrix4(CDF_TO_VIEWER_TRANSFORMATION.clone().invert()); + return { positionX: cdfPosition.x, positionY: cdfPosition.y, positionZ: cdfPosition.z }; +} + +const poiMarker = Symbol('poiMarker'); + +export type PointsOfInterestIntersection = Omit< + DomainObjectIntersection, + 'userData' | 'domainObject' +> & { + marker: typeof poiMarker; + domainObject: PointsOfInterestDomainObject; + userData: PointsOfInterest; +}; + +export function createPointsOfInterestIntersection( + point: Vector3, + distanceToCamera: number, + customObject: ICustomObject, + domainObject: PointsOfInterestDomainObject, + overlay: PointsOfInterest +): PointsOfInterestIntersection { + return { + type: 'customObject', + marker: poiMarker, + point, + distanceToCamera, + customObject, + domainObject, + userData: overlay + }; +} + +export function isPointsOfInterestIntersection( + objectIntersection: AnyIntersection +): objectIntersection is PointsOfInterestIntersection { + return (objectIntersection as PointsOfInterestIntersection).marker === poiMarker; +} diff --git a/react-components/src/components/Architecture/RevealButtons.tsx b/react-components/src/components/Architecture/RevealButtons.tsx index 533561d8790..5ae28b19307 100644 --- a/react-components/src/components/Architecture/RevealButtons.tsx +++ b/react-components/src/components/Architecture/RevealButtons.tsx @@ -11,7 +11,7 @@ import { SetAxisVisibleCommand } from '../../architecture/concrete/axis/SetAxisV import { ClipTool } from '../../architecture/concrete/clipping/ClipTool'; import { MeasurementTool } from '../../architecture/concrete/measurements/MeasurementTool'; import { KeyboardSpeedCommand } from '../../architecture/base/concreteCommands/KeyboardSpeedCommand'; -import { ObservationsTool } from '../../architecture/concrete/observations/ObservationsTool'; +import { PointsOfInterestTool } from '../../architecture/concrete/pointsOfInterest/PointsOfInterestTool'; import { createButtonFromCommandConstructor } from './CommandButtons'; import { SettingsCommand } from '../../architecture/base/concreteCommands/SettingsCommand'; import { PointCloudFilterCommand } from '../../architecture'; @@ -56,8 +56,8 @@ export class RevealButtons { () => new SetFlexibleControlsTypeCommand(FlexibleControlsType.FirstPerson) ); - static Observations = (): ReactElement => { - return createButtonFromCommandConstructor(() => new ObservationsTool()); + static PointsOfInterest = (): ReactElement => { + return createButtonFromCommandConstructor(() => new PointsOfInterestTool()); }; static KeyboardSpeed = (): ReactElement => diff --git a/react-components/src/data-providers/core-dm-provider/CoreDm3dDataProvider.ts b/react-components/src/data-providers/core-dm-provider/CoreDm3dDataProvider.ts index a43931f9d7d..a2146f7e511 100644 --- a/react-components/src/data-providers/core-dm-provider/CoreDm3dDataProvider.ts +++ b/react-components/src/data-providers/core-dm-provider/CoreDm3dDataProvider.ts @@ -27,7 +27,7 @@ import { filterNodesByMappedTo3d } from './filterNodesByMappedTo3d'; import { getCadModelsForInstance } from './getCadModelsForInstance'; import { getCadConnectionsForRevisions } from './getCadConnectionsForRevisions'; import { zip } from 'lodash'; -import { restrictToDmsId } from './restrictToDmsId'; +import { restrictToDmsId } from '../../utilities/restrictToDmsId'; const MAX_PARALLEL_QUERIES = 2; diff --git a/react-components/src/data-providers/core-dm-provider/getCadConnectionsForRevisions.ts b/react-components/src/data-providers/core-dm-provider/getCadConnectionsForRevisions.ts index 252a20e8545..32515f1c960 100644 --- a/react-components/src/data-providers/core-dm-provider/getCadConnectionsForRevisions.ts +++ b/react-components/src/data-providers/core-dm-provider/getCadConnectionsForRevisions.ts @@ -17,7 +17,7 @@ import { createFdmKey } from '../../components/CacheProvider/idAndKeyTranslation import { type PromiseType } from '../utils/typeUtils'; import { isDefined } from '../../utilities/isDefined'; import { type QueryResult } from '../utils/queryNodesAndEdges'; -import { restrictToDmsId } from './restrictToDmsId'; +import { restrictToDmsId } from '../../utilities/restrictToDmsId'; import { cadConnectionsQuery } from './cadConnectionsQuery'; export async function getCadConnectionsForRevisions( diff --git a/react-components/src/data-providers/core-dm-provider/getDMSRevision.ts b/react-components/src/data-providers/core-dm-provider/getDMSRevision.ts index 3d93a3eee3a..78cd3d36970 100644 --- a/react-components/src/data-providers/core-dm-provider/getDMSRevision.ts +++ b/react-components/src/data-providers/core-dm-provider/getDMSRevision.ts @@ -4,7 +4,7 @@ import { type QueryRequest } from '@cognite/sdk'; import { type DmsUniqueIdentifier, type FdmSDK, type NodeItem } from '../FdmSDK'; import { type COGNITE_CAD_REVISION_SOURCE, type CogniteCADRevisionProperties } from './dataModels'; -import { restrictToDmsId } from './restrictToDmsId'; +import { restrictToDmsId } from '../../utilities/restrictToDmsId'; import { revisionQuery } from './revisionQuery'; export async function getDMSRevision( diff --git a/react-components/src/data-providers/core-dm-provider/getFdmConnectionsForNodes.ts b/react-components/src/data-providers/core-dm-provider/getFdmConnectionsForNodes.ts index 69b42417757..e3782e18f23 100644 --- a/react-components/src/data-providers/core-dm-provider/getFdmConnectionsForNodes.ts +++ b/react-components/src/data-providers/core-dm-provider/getFdmConnectionsForNodes.ts @@ -21,7 +21,7 @@ import { import { getModelIdFromExternalId } from './getCdfIdFromExternalId'; import { createFdmKey } from '../../components/CacheProvider/idAndKeyTranslation'; import { cogniteCadNodeSourceWithProperties } from './cogniteCadNodeSourceWithProperties'; -import { restrictToDmsId } from './restrictToDmsId'; +import { restrictToDmsId } from '../../utilities/restrictToDmsId'; export async function getFdmConnectionsForNodes( model: DmsUniqueIdentifier, diff --git a/react-components/src/data-providers/core-dm-provider/restrictToDmsId.ts b/react-components/src/utilities/restrictToDmsId.ts similarity index 75% rename from react-components/src/data-providers/core-dm-provider/restrictToDmsId.ts rename to react-components/src/utilities/restrictToDmsId.ts index 3e0cb5d4ccc..c42bdbfe80b 100644 --- a/react-components/src/data-providers/core-dm-provider/restrictToDmsId.ts +++ b/react-components/src/utilities/restrictToDmsId.ts @@ -1,7 +1,7 @@ /*! * Copyright 2024 Cognite AS */ -import { type DmsUniqueIdentifier } from '../FdmSDK'; +import { type DmsUniqueIdentifier } from '../data-providers/FdmSDK'; export function restrictToDmsId(identifier: T): DmsUniqueIdentifier { return { externalId: identifier.externalId, space: identifier.space };