From cff071ed49537495a800f37883dccb898edf950a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Flatval?= <70905152+haakonflatval-cognite@users.noreply.github.com> Date: Wed, 16 Aug 2023 17:31:10 +0200 Subject: [PATCH] feat: shared 3D-to-FDM node cache (#3588) * feat: shared 3D-to-FDM node cache --- .../NodeCacheProvider/FdmNodeCache.ts | 283 ++++++++++++++++++ .../NodeCacheProvider/NodeCacheProvider.tsx | 82 +++++ .../NodeCacheProvider/RevisionFdmNodeCache.ts | 235 +++++++++++++++ .../components/NodeCacheProvider/requests.ts | 128 ++++++++ .../src/components/NodeCacheProvider/types.ts | 21 ++ .../src/components/Reveal3DResources/types.ts | 2 +- .../RevealContainer/RevealContainer.tsx | 5 +- .../src/hooks/useCalculateModelsStyling.tsx | 13 +- react-components/src/hooks/useClickedNode.tsx | 21 +- .../useMappedEquipmentBy3DModelsList.tsx | 74 ----- .../src/hooks/useNodeMappedData.tsx | 161 ---------- .../stories/HighlightNode.stories.tsx | 6 +- 12 files changed, 783 insertions(+), 248 deletions(-) create mode 100644 react-components/src/components/NodeCacheProvider/FdmNodeCache.ts create mode 100644 react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx create mode 100644 react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts create mode 100644 react-components/src/components/NodeCacheProvider/requests.ts create mode 100644 react-components/src/components/NodeCacheProvider/types.ts delete mode 100644 react-components/src/hooks/useMappedEquipmentBy3DModelsList.tsx delete mode 100644 react-components/src/hooks/useNodeMappedData.tsx diff --git a/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts new file mode 100644 index 00000000000..079911708af --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/FdmNodeCache.ts @@ -0,0 +1,283 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type Node3D, type CogniteClient } from '@cognite/sdk'; +import { type EdgeItem, type FdmSDK } from '../../utilities/FdmSDK'; +import { RevisionFdmNodeCache } from './RevisionFdmNodeCache'; +import { + type FdmEdgeWithNode, + type Fdm3dNodeData, + type FdmCadEdge, + type RevisionKey, + type RevisionTreeIndex, + type FdmKey, + type FdmId, + type RevisionId, + type NodeId, + type ModelNodeIdKey, + type ModelId +} from './types'; +import { + type InModel3dEdgeProperties, + SYSTEM_3D_EDGE_SOURCE, + SYSTEM_SPACE_3D_SCHEMA +} from '../../utilities/globalDataModels'; + +import { partition } from 'lodash'; + +import assert from 'assert'; +import { fetchNodesForNodeIds } from './requests'; + +export type ModelRevisionKey = `${number}-${number}`; +export type ModelRevisionToEdgeMap = Map; + +export class FdmNodeCache { + private readonly _revisionNodeCaches = new Map(); + + private readonly _cdfClient: CogniteClient; + private readonly _fdmClient: FdmSDK; + + private readonly _completeRevisions = new Set(); + + public constructor(cdfClient: CogniteClient, fdmClient: FdmSDK) { + this._cdfClient = cdfClient; + this._fdmClient = fdmClient; + } + + public async getAllMappingExternalIds( + modelRevisionIds: Array<{ modelId: number; revisionId: number }> + ): Promise { + const [cachedRevisionIds, nonCachedRevisionIds] = partition(modelRevisionIds, (ids) => { + const key = createRevisionKey(ids.modelId, ids.revisionId); + return this._completeRevisions.has(key); + }); + + const cachedEdges = cachedRevisionIds.map((id) => this.getCachedEdgesForRevision(id)); + + const revisionToEdgesMap = await this.getRevisionToEdgesMap(nonCachedRevisionIds); + + this.writeRevisionDataToCache(revisionToEdgesMap); + + cachedEdges.forEach(([revisionKey, edges]) => { + revisionToEdgesMap.set(revisionKey, edges); + }); + + return revisionToEdgesMap; + } + + private getCachedEdgesForRevision(id: { + modelId: number; + revisionId: number; + }): [RevisionKey, FdmEdgeWithNode[]] { + const revisionCache = this.getOrCreateRevisionCache(id.modelId, id.revisionId); + const revisionKey = createRevisionKey(id.modelId, id.revisionId); + const cachedRevisionEdges = revisionCache.getAllEdges(); + return [revisionKey, cachedRevisionEdges]; + } + + private writeRevisionDataToCache(modelMap: Map): void { + for (const [revisionKey, data] of modelMap.entries()) { + const [modelId, revisionId] = revisionKeyToIds(revisionKey); + const revisionCache = this.getOrCreateRevisionCache(modelId, revisionId); + + data.forEach((edgeAndNode) => { + revisionCache.insertTreeIndexMappings(edgeAndNode.node.treeIndex, edgeAndNode); + }); + + this._completeRevisions.add(revisionKey); + } + } + + private async getRevisionToEdgesMap( + modelRevisionIds: Array<{ modelId: number; revisionId: number }> + ): Promise> { + const revisionIds = modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId); + const edges = await this.getEdgesForRevisions(revisionIds, this._fdmClient); + return await groupToModelRevision(edges, modelRevisionIds, this._cdfClient); + } + + public async getClosestParentExternalId( + modelId: number, + revisionId: number, + treeIndex: number + ): Promise { + const revisionCache = this.getOrCreateRevisionCache(modelId, revisionId); + + return await revisionCache.getClosestParentFdmData(treeIndex); + } + + private async getEdgesForRevisions( + revisionIds: number[], + fdmClient: FdmSDK + ): Promise>> { + const versionedPropertiesKey = `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`; + const filter = { + in: { + property: [SYSTEM_SPACE_3D_SCHEMA, versionedPropertiesKey, 'revisionId'], + values: revisionIds + } + }; + const mappings = await fdmClient.filterAllInstances( + filter, + 'edge', + SYSTEM_3D_EDGE_SOURCE + ); + return mappings.edges; + } + + private getOrCreateRevisionCache(modelId: number, revisionId: number): RevisionFdmNodeCache { + const revisionKey = createRevisionKey(modelId, revisionId); + + const revisionCache = this._revisionNodeCaches.get(revisionKey); + + if (revisionCache !== undefined) { + return revisionCache; + } + + const newRevisionCache = new RevisionFdmNodeCache( + this._cdfClient, + this._fdmClient, + modelId, + revisionId + ); + + this._revisionNodeCaches.set(revisionKey, newRevisionCache); + + return newRevisionCache; + } +} + +function createRevisionKey(modelId: number, revisionId: number): RevisionKey { + return `${modelId}-${revisionId}`; +} + +function revisionKeyToIds(revisionKey: RevisionKey): [number, number] { + const components = revisionKey.split('-'); + return [Number(components[0]), Number(components[1])]; +} + +export function createRevisionTreeIndex( + modelId: number, + revisionId: number, + treeIndex: number +): RevisionTreeIndex { + return `${modelId}-${revisionId}-${treeIndex}`; +} + +export function createFdmKey(spaceId: string, externalId: string): FdmKey { + return `${spaceId}-${externalId}`; +} + +export function fdmKeyToId(fdmKey: FdmKey): FdmId { + const parts = fdmKey.split('-'); + + return { space: parts[0], externalId: parts[1] }; +} + +export function insertIntoSetMap(key: T, value: U, globalMap: Map): void { + const prevVal = globalMap.get(key); + + if (prevVal === undefined) { + globalMap.set(key, [value]); + return; + } + + prevVal.push(value); +} + +async function groupToModelRevision( + edges: FdmCadEdge[], + modelRevisionIds: Array<{ modelId: number; revisionId: number }>, + cdfClient: CogniteClient +): Promise> { + const revisionToNodeIdsMap = createRevisionToNodeIdMap(edges); + const modelNodeIdToNodeMap = await createModelNodeIdToNodeMap( + revisionToNodeIdsMap, + modelRevisionIds, + cdfClient + ); + + return edges.reduce((map, edge) => { + const edgeRevisionId = edge.properties.revisionId; + const modelRevisionId = modelRevisionIds.find((p) => p.revisionId === edgeRevisionId); + + if (modelRevisionId === undefined) return map; + + const value = createFdmEdgeWithNode(modelRevisionId, edge, modelNodeIdToNodeMap); + + insertEdgeIntoMapList(value, map, modelRevisionId); + + return map; + }, new Map()); +} + +function createFdmEdgeWithNode( + modelRevisionId: { modelId: number; revisionId: number }, + edge: FdmCadEdge, + modelNodeIdToNodeMap: Map +): FdmEdgeWithNode { + const revisionNodeIdKey = + `${modelRevisionId.modelId}-${modelRevisionId.revisionId}-${edge.properties.revisionNodeId}` as const; + + const node = modelNodeIdToNodeMap.get(revisionNodeIdKey); + assert(node !== undefined); + + return { edge, node }; +} + +function insertEdgeIntoMapList( + value: FdmEdgeWithNode, + map: Map, + modelRevisionId: { modelId: number; revisionId: number } +): void { + const modelRevisionIdKey: ModelRevisionKey = createRevisionKey( + modelRevisionId.modelId, + modelRevisionId.revisionId + ); + + const edgesForModel = map.get(modelRevisionIdKey); + + if (edgesForModel === undefined) { + map.set(modelRevisionIdKey, [value]); + } else { + edgesForModel.push(value); + } +} + +async function createModelNodeIdToNodeMap( + revisionToNodeIdsMap: Map, + modelRevisionIds: Array<{ modelId: ModelId; revisionId: RevisionId }>, + cdfClient: CogniteClient +): Promise> { + const revisionNodeIdToNode = new Map(); + + const nodePromises = [...revisionToNodeIdsMap.entries()].map(async ([revisionId, nodeIds]) => { + const modelId = modelRevisionIds.find((p) => p.revisionId === revisionId)?.modelId; + assert(modelId !== undefined); + + const nodes = await fetchNodesForNodeIds(modelId, revisionId, nodeIds, cdfClient); + nodeIds.forEach((e, ind) => { + const modelNodeIdKey = `${modelId}-${revisionId}-${e}` as const; + revisionNodeIdToNode.set(modelNodeIdKey, nodes[ind]); + }); + }); + + await Promise.all(nodePromises); + + return revisionNodeIdToNode; +} + +function createRevisionToNodeIdMap(edges: FdmCadEdge[]): Map { + return edges.reduce((revisionNodeIdMap, edge) => { + const nodeIdsInRevision = revisionNodeIdMap.get(edge.properties.revisionId); + + if (nodeIdsInRevision !== undefined) { + nodeIdsInRevision.push(edge.properties.revisionNodeId); + } else { + revisionNodeIdMap.set(edge.properties.revisionId, [edge.properties.revisionNodeId]); + } + + return revisionNodeIdMap; + }, new Map()); +} diff --git a/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx b/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx new file mode 100644 index 00000000000..4b7cc2e015c --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/NodeCacheProvider.tsx @@ -0,0 +1,82 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type ReactElement, type ReactNode, createContext, useContext, useMemo } from 'react'; +import { FdmNodeCache, type ModelRevisionToEdgeMap } from './FdmNodeCache'; +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { useFdmSdk, useSDK } from '../RevealContainer/SDKProvider'; +import { type Fdm3dNodeData } from './types'; + +import assert from 'assert'; + +export type FdmNodeCacheContent = { + cache: FdmNodeCache; +}; + +export const FdmNodeCacheContext = createContext(undefined); + +export const useMappedEdgesForRevisions = ( + modelRevisionIds: Array<{ modelId: number; revisionId: number }>, + enabled: boolean +): UseQueryResult => { + const content = useContext(FdmNodeCacheContext); + + if (content === undefined) { + throw Error('Must use useNodeCache inside a NodeCacheContext'); + } + + return useQuery( + [ + 'reveal', + 'react-components', + ...modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId.toString()).sort() + ], + async () => await content.cache.getAllMappingExternalIds(modelRevisionIds), + { staleTime: Infinity, enabled: enabled && modelRevisionIds.length > 0 } + ); +}; + +export const useFdm3dNodeData = ( + modelId: number | undefined, + revisionId: number | undefined, + treeIndex: number | undefined +): UseQueryResult => { + const content = useContext(FdmNodeCacheContext); + + const enableQuery = + content !== undefined && + modelId !== undefined && + revisionId !== undefined && + treeIndex !== undefined; + + const result = useQuery( + ['reveal', 'react-components', 'tree-index-to-external-id', modelId, revisionId, treeIndex], + async () => { + assert(enableQuery); + return await content.cache.getClosestParentExternalId(modelId, revisionId, treeIndex); + }, + { + enabled: enableQuery + } + ); + + if (content === undefined) { + throw Error('Must use useNodeCache inside a NodeCacheContext'); + } + + return result; +}; + +export function NodeCacheProvider({ children }: { children?: ReactNode }): ReactElement { + const fdmClient = useFdmSdk(); + const cdfClient = useSDK(); + + const fdmCache = useMemo(() => new FdmNodeCache(cdfClient, fdmClient), []); + + return ( + + {children} + + ); +} diff --git a/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts b/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts new file mode 100644 index 00000000000..02b75e531e1 --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/RevisionFdmNodeCache.ts @@ -0,0 +1,235 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type CogniteClient, type Node3D } from '@cognite/sdk'; +import { type FdmSDK } from '../../utilities/FdmSDK'; +import { type TreeIndex, type Fdm3dNodeData, type FdmEdgeWithNode, type FdmCadEdge } from './types'; + +import { + fetchAncestorNodesForTreeIndex, + getMappingEdgesForNodeIds, + inspectNodes +} from './requests'; + +import { max } from 'lodash'; + +import assert from 'assert'; + +export class RevisionFdmNodeCache { + private readonly _cogniteClient: CogniteClient; + private readonly _fdmClient: FdmSDK; + + private readonly _modelId: number; + private readonly _revisionId: number; + + private readonly _treeIndexToFdmEdges = new Map(); + private readonly _treeIndexToFdmData = new Map(); + + constructor( + cogniteClient: CogniteClient, + fdmClient: FdmSDK, + modelId: number, + revisionId: number + ) { + this._cogniteClient = cogniteClient; + this._fdmClient = fdmClient; + + this._modelId = modelId; + this._revisionId = revisionId; + } + + public async getClosestParentFdmData(searchTreeIndex: number): Promise { + const cachedFdmData = this._treeIndexToFdmData.get(searchTreeIndex); + + if (cachedFdmData !== undefined) { + return cachedFdmData; + } + + const cachedFdmEdges = this._treeIndexToFdmEdges.get(searchTreeIndex); + + if (cachedFdmEdges !== undefined) { + return await this.getDataWithViewsForFdmEdges(cachedFdmEdges, []); + } + + return await this.findNodeDataFromAncestors(searchTreeIndex); + } + + private async findNodeDataFromAncestors(treeIndex: TreeIndex): Promise { + const { edges, ancestorsWithSameMapping, firstMappedAncestorTreeIndex } = + await this.getClosestParentMapping(treeIndex); + + if (edges.length === 0) { + return []; + } + + const cachedFdmData = this._treeIndexToFdmData.get(firstMappedAncestorTreeIndex); + + if (cachedFdmData !== undefined) { + this.setCacheForNodes(ancestorsWithSameMapping, cachedFdmData); + + return cachedFdmData; + } + + const firstMappedAncestor = ancestorsWithSameMapping.find( + (ancestor) => ancestor.treeIndex === firstMappedAncestorTreeIndex + ); + + assert(firstMappedAncestor !== undefined); + + const nodeEdges = edges.map((edge) => ({ edge, node: firstMappedAncestor })); + + return await this.getDataWithViewsForFdmEdges(nodeEdges, ancestorsWithSameMapping); + } + + private setCacheForNodes(nodes: Node3D[], nodeData: Fdm3dNodeData[]): void { + nodes.forEach((node) => { + this._treeIndexToFdmData.set(node.treeIndex, nodeData); + }); + } + + private async getDataWithViewsForFdmEdges( + nodeEdges: FdmEdgeWithNode[], + ancestorsWithSameMapping: Node3D[] + ): Promise { + const nodeInspectionResults = await inspectNodes( + this._fdmClient, + nodeEdges.map((edge) => edge.edge.startNode) + ); + + const dataWithViews = nodeEdges.map((fdmEdgeWithNode, ind) => ({ + fdmId: fdmEdgeWithNode.edge.startNode, + view: nodeInspectionResults.items[ind].inspectionResults.involvedViewsAndContainers.views[0], + cadNode: fdmEdgeWithNode.node + })); + + ancestorsWithSameMapping.forEach((ancestor) => + this._treeIndexToFdmData.set(ancestor.treeIndex, dataWithViews) + ); + + return dataWithViews; + } + + private async getClosestParentMapping(treeIndex: number): Promise<{ + edges: FdmCadEdge[]; + ancestorsWithSameMapping: Node3D[]; + firstMappedAncestorTreeIndex: number; + }> { + const ancestors: Node3D[] = await fetchAncestorNodesForTreeIndex( + this._modelId, + this._revisionId, + treeIndex, + this._cogniteClient + ); + + const ancestorMappings = await this.getMappingEdgesForAncestors(ancestors); + + if (ancestorMappings.length === 0) { + return { edges: [], ancestorsWithSameMapping: [], firstMappedAncestorTreeIndex: 0 }; + } + + const edgesWithCorrespondingTreeIndex = this.combineEdgesWithTreeIndex( + ancestorMappings, + ancestors + ); + + const firstMappedAncestorTreeIndex = findLargestTreeIndex(edgesWithCorrespondingTreeIndex); + return getAncestorDataForTreeIndex( + firstMappedAncestorTreeIndex, + edgesWithCorrespondingTreeIndex, + ancestors + ); + } + + private combineEdgesWithTreeIndex( + mappingEdges: FdmCadEdge[], + nodes: Node3D[] + ): Array<{ edge: FdmCadEdge; treeIndex: TreeIndex }> { + return mappingEdges.map((edge) => { + const ancestorConnectedToEdge = nodes.find( + (ancestor) => ancestor.id === edge.properties.revisionNodeId + ); + + assert(ancestorConnectedToEdge !== undefined); + + return { + edge, + treeIndex: ancestorConnectedToEdge.treeIndex + }; + }); + } + + private async getMappingEdgesForAncestors(ancestors: Node3D[]): Promise { + const cachedFirstMappedAncestor = ancestors + .filter((ancestor) => this._treeIndexToFdmEdges.has(ancestor.treeIndex)) + .sort((nodeA, nodeB) => nodeB.treeIndex - nodeA.treeIndex)[0]; + + if (cachedFirstMappedAncestor !== undefined) { + const edgesAndNodes = this._treeIndexToFdmEdges.get(cachedFirstMappedAncestor.treeIndex); + + assert(edgesAndNodes !== undefined); + + return edgesAndNodes.map((edge) => edge.edge); + } + + const ancestorMappings = await getMappingEdgesForNodeIds( + this._modelId, + this._revisionId, + this._fdmClient, + ancestors.map((a) => a.id) + ); + + return ancestorMappings.edges; + } + + public insertTreeIndexMappings(treeIndex: TreeIndex, edge: FdmEdgeWithNode): void { + const edgeArray = this._treeIndexToFdmEdges.get(treeIndex); + if (edgeArray === undefined) { + this._treeIndexToFdmEdges.set(treeIndex, [edge]); + } else { + edgeArray.push(edge); + } + } + + public getAllEdges(): FdmEdgeWithNode[] { + return [...this._treeIndexToFdmEdges.values()].flat(); + } + + getIds(): { modelId: number; revisionId: number } { + return { + modelId: this._modelId, + revisionId: this._revisionId + }; + } +} + +function findLargestTreeIndex( + edgesWithTreeIndex: Array<{ edge: FdmCadEdge; treeIndex: TreeIndex }> +): TreeIndex { + const maxTreeIndex = max(edgesWithTreeIndex.map((e) => e.treeIndex)); + assert(maxTreeIndex !== undefined); + return maxTreeIndex; +} + +function getAncestorDataForTreeIndex( + treeIndex: TreeIndex, + edgesWithTreeIndex: Array<{ edge: FdmCadEdge; treeIndex: TreeIndex }>, + ancestors: Node3D[] +): { + edges: FdmCadEdge[]; + ancestorsWithSameMapping: Node3D[]; + firstMappedAncestorTreeIndex: number; +} { + const edgesForFirstMappedAncestor = edgesWithTreeIndex.filter( + (edgeAndTreeIndex) => edgeAndTreeIndex.treeIndex === treeIndex + ); + const ancestorsBetweenSearchNodeAndFirstMappedAncestor = ancestors.filter( + (ancestor) => ancestor.treeIndex >= treeIndex + ); + + return { + edges: edgesForFirstMappedAncestor.map((result) => result.edge), + ancestorsWithSameMapping: ancestorsBetweenSearchNodeAndFirstMappedAncestor, + firstMappedAncestorTreeIndex: treeIndex + }; +} diff --git a/react-components/src/components/NodeCacheProvider/requests.ts b/react-components/src/components/NodeCacheProvider/requests.ts new file mode 100644 index 00000000000..42829f77a3a --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/requests.ts @@ -0,0 +1,128 @@ +/*! + * Copyright 2023 Cognite AS + */ + +import { type CogniteClient, type CogniteInternalId, type Node3D } from '@cognite/sdk'; +import { + type DmsUniqueIdentifier, + type FdmSDK, + type InspectResultList +} from '../../utilities/FdmSDK'; +import { type FdmCadEdge } from './types'; +import { + INSTANCE_SPACE_3D_DATA, + type InModel3dEdgeProperties, + SYSTEM_3D_EDGE_SOURCE +} from '../../utilities/globalDataModels'; + +export async function fetchAncestorNodesForTreeIndex( + modelId: number, + revisionId: number, + treeIndex: number, + cogniteClient: CogniteClient +): Promise { + const nodeId = await treeIndexesToNodeIds(modelId, revisionId, [treeIndex], cogniteClient); + + const ancestorNodes = await cogniteClient.revisions3D.list3DNodeAncestors( + modelId, + revisionId, + nodeId[0] + ); + + return ancestorNodes.items; +} + +export async function getMappingEdgesForNodeIds( + modelId: number, + revisionId: number, + fdmClient: FdmSDK, + ancestorIds: CogniteInternalId[] +): Promise<{ edges: FdmCadEdge[] }> { + const filter = { + and: [ + { + equals: { + property: ['edge', 'endNode'], + value: { + space: INSTANCE_SPACE_3D_DATA, + externalId: `${modelId}` + } + } + }, + { + equals: { + property: [ + SYSTEM_3D_EDGE_SOURCE.space, + `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, + 'revisionId' + ], + value: revisionId + } + }, + { + in: { + property: [ + SYSTEM_3D_EDGE_SOURCE.space, + `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, + 'revisionNodeId' + ], + values: ancestorIds + } + } + ] + }; + + return await fdmClient.filterAllInstances( + filter, + 'edge', + SYSTEM_3D_EDGE_SOURCE + ); +} + +export async function inspectNodes( + fdmClient: FdmSDK, + dataNodes: DmsUniqueIdentifier[] +): Promise { + const inspectionResult = await fdmClient.inspectInstances({ + inspectionOperations: { involvedViewsAndContainers: {} }, + items: dataNodes.map((node) => ({ + instanceType: 'node', + externalId: node.externalId, + space: node.space + })) + }); + + return inspectionResult; +} + +export async function treeIndexesToNodeIds( + modelId: number, + revisionId: number, + treeIndexes: number[], + cogniteClient: CogniteClient +): Promise { + const outputsUrl = `${cogniteClient.getBaseUrl()}/api/v1/projects/${ + cogniteClient.project + }/3d/models/${modelId}/revisions/${revisionId}/nodes/internalids/bytreeindices`; + const response = await cogniteClient.post<{ items: number[] }>(outputsUrl, { + data: { items: treeIndexes } + }); + if (response.status === 200) { + return response.data.items; + } else { + throw Error(`treeIndex-nodeId translation failed for treeIndexes ${treeIndexes.join(',')}`); + } +} + +export async function fetchNodesForNodeIds( + modelId: number, + revisionId: number, + nodeIds: number[], + cogniteClient: CogniteClient +): Promise { + return await cogniteClient.revisions3D.retrieve3DNodes( + modelId, + revisionId, + nodeIds.map((id) => ({ id })) + ); +} diff --git a/react-components/src/components/NodeCacheProvider/types.ts b/react-components/src/components/NodeCacheProvider/types.ts new file mode 100644 index 00000000000..8d4ad971242 --- /dev/null +++ b/react-components/src/components/NodeCacheProvider/types.ts @@ -0,0 +1,21 @@ +/*! + * Copyright 2023 Cognite AS + */ +import { type Node3D } from '@cognite/sdk'; +import { type EdgeItem, type DmsUniqueIdentifier, type Source } from '../../utilities/FdmSDK'; +import { type InModel3dEdgeProperties } from '../../utilities/globalDataModels'; + +export type Fdm3dNodeData = { fdmId: DmsUniqueIdentifier; view: Source; cadNode: Node3D }; +export type FdmCadEdge = EdgeItem; +export type FdmEdgeWithNode = { edge: FdmCadEdge; node: Node3D }; + +export type ModelId = number; +export type RevisionId = number; +export type TreeIndex = number; +export type NodeId = number; +export type FdmId = DmsUniqueIdentifier; + +export type RevisionKey = `${ModelId}-${RevisionId}`; +export type FdmKey = `${string}-${string}`; +export type RevisionTreeIndex = `${ModelId}-${RevisionId}-${TreeIndex}`; +export type ModelNodeIdKey = `${ModelId}-${RevisionId}-${NodeId}`; diff --git a/react-components/src/components/Reveal3DResources/types.ts b/react-components/src/components/Reveal3DResources/types.ts index c98d13c83f7..16356a4258f 100644 --- a/react-components/src/components/Reveal3DResources/types.ts +++ b/react-components/src/components/Reveal3DResources/types.ts @@ -26,7 +26,7 @@ export type AddReveal3DModelOptions = AddModelOptions & { transform?: Matrix4 } export type TypedReveal3DModel = AddReveal3DModelOptions & { type: SupportedModelTypes }; export type NodeDataResult = { - nodeExternalId: string; + fdmNode: DmsUniqueIdentifier; view: Source; cadNode: Node3D; }; diff --git a/react-components/src/components/RevealContainer/RevealContainer.tsx b/react-components/src/components/RevealContainer/RevealContainer.tsx index 825214d4308..7fed7ca89d4 100644 --- a/react-components/src/components/RevealContainer/RevealContainer.tsx +++ b/react-components/src/components/RevealContainer/RevealContainer.tsx @@ -10,6 +10,7 @@ import { type Color } from 'three'; import { SDKProvider } from './SDKProvider'; import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; import { useRevealKeepAlive } from '../RevealKeepAlive/RevealKeepAliveContext'; +import { NodeCacheProvider } from '../NodeCacheProvider/NodeCacheProvider'; import { RevealContainerElementContext } from './RevealContainerElementContext'; type RevealContainerProps = { @@ -77,7 +78,9 @@ export function RevealContainer({ <> - {createPortal(children, viewerDomElement.current)} + + {createPortal(children, viewerDomElement.current)} + diff --git a/react-components/src/hooks/useCalculateModelsStyling.tsx b/react-components/src/hooks/useCalculateModelsStyling.tsx index 153fb453ead..0e60723696d 100644 --- a/react-components/src/hooks/useCalculateModelsStyling.tsx +++ b/react-components/src/hooks/useCalculateModelsStyling.tsx @@ -10,7 +10,6 @@ import { type NodeStylingGroup, type CadModelStyling } from '../components/CadModelContainer/CadModelContainer'; -import { useMappedEquipmentByRevisionList } from './useMappedEquipmentBy3DModelsList'; import { type InModel3dEdgeProperties } from '../utilities/globalDataModels'; import { type EdgeItem } from '../utilities/FdmSDK'; import { type NodeAppearance } from '@cognite/reveal'; @@ -18,6 +17,7 @@ import { type ThreeDModelMappings } from './types'; import { type CogniteExternalId, type CogniteInternalId } from '@cognite/sdk'; import { useFdmAssetMappings } from './useFdmAssetMappings'; import { useEffect, useMemo } from 'react'; +import { useMappedEdgesForRevisions } from '../components/NodeCacheProvider/NodeCacheProvider'; type ModelStyleGroup = { model: TypedReveal3DModel; @@ -43,7 +43,7 @@ function useCalculateMappedStyling(models: TypedReveal3DModel[]): ModelStyleGrou (model) => model.styling?.mapped !== undefined ); const shouldFetchAllMappedEquipment = modelsRevisionsWithMappedEquipment.length > 0; - const { data: mappedEquipmentEdges } = useMappedEquipmentByRevisionList( + const { data: mappedEquipmentEdges } = useMappedEdgesForRevisions( modelsRevisionsWithMappedEquipment, shouldFetchAllMappedEquipment ); @@ -57,11 +57,16 @@ function useCalculateMappedStyling(models: TypedReveal3DModel[]): ModelStyleGrou return []; } return models.map((model) => { - const edges = mappedEquipmentEdges?.get(`${model.modelId}-${model.revisionId}`) ?? []; + const fdmData = mappedEquipmentEdges?.get(`${model.modelId}-${model.revisionId}`) ?? []; const styleGroup = model.styling?.mapped !== undefined - ? [getMappedStyleGroup(edges, model.styling.mapped)] + ? [ + getMappedStyleGroup( + fdmData.map((data) => data.edge), + model.styling.mapped + ) + ] : []; return { model, styleGroup }; }); diff --git a/react-components/src/hooks/useClickedNode.tsx b/react-components/src/hooks/useClickedNode.tsx index a4d87abee75..b1ee93b6b00 100644 --- a/react-components/src/hooks/useClickedNode.tsx +++ b/react-components/src/hooks/useClickedNode.tsx @@ -5,7 +5,7 @@ import { type CadIntersection, type PointerEventData } from '@cognite/reveal'; import { useReveal, type NodeDataResult } from '../'; import { useEffect, useState } from 'react'; -import { useNodeMappedData } from './useNodeMappedData'; +import { useFdm3dNodeData } from '../components/NodeCacheProvider/NodeCacheProvider'; export type ClickedNodeData = NodeDataResult & { intersection: CadIntersection; @@ -36,10 +36,23 @@ export const useClickedNodeData = (): ClickedNodeData | undefined => { }; }, [viewer]); - const nodeData = useNodeMappedData(cadIntersection?.treeIndex, cadIntersection?.model); + const nodeData = + useFdm3dNodeData( + cadIntersection?.model.modelId, + cadIntersection?.model.revisionId, + cadIntersection?.treeIndex + ).data ?? []; - if (nodeData === undefined || cadIntersection === undefined) { + if (cadIntersection === undefined || nodeData.length === 0) { return undefined; } - return { intersection: cadIntersection, ...nodeData }; + + const chosenNode = nodeData[0]; + + return { + intersection: cadIntersection, + fdmNode: chosenNode.fdmId, + view: chosenNode.view, + cadNode: chosenNode.cadNode + }; }; diff --git a/react-components/src/hooks/useMappedEquipmentBy3DModelsList.tsx b/react-components/src/hooks/useMappedEquipmentBy3DModelsList.tsx deleted file mode 100644 index 1b3239a652e..00000000000 --- a/react-components/src/hooks/useMappedEquipmentBy3DModelsList.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ -import { type UseQueryResult, useQuery } from '@tanstack/react-query'; -import { useFdmSdk } from '../components/RevealContainer/SDKProvider'; -import { - type InModel3dEdgeProperties, - SYSTEM_3D_EDGE_SOURCE, - SYSTEM_SPACE_3D_SCHEMA -} from '../utilities/globalDataModels'; -import { type FdmSDK, type EdgeItem } from '../utilities/FdmSDK'; - -export type ModelRevisionId = `${number}-${number}`; -export type ModelRevisionToEdgeMap = Map>>; - -export const useMappedEquipmentByRevisionList = ( - modelRevisionIds: Array<{ modelId: number; revisionId: number }>, - enabled = true -): UseQueryResult => { - const fdmClient = useFdmSdk(); - return useQuery( - [ - 'reveal', - 'react-components', - ...modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId.toString()).sort() - ], - async () => { - const revisionIds = modelRevisionIds.map((modelRevisionId) => modelRevisionId.revisionId); - const edges = await getEdgesForRevisions(revisionIds, fdmClient); - const groupToModels = groupToModelRevision(edges, modelRevisionIds); - return groupToModels; - }, - { staleTime: Infinity, enabled: enabled && modelRevisionIds.length > 0 } - ); -}; - -function groupToModelRevision( - edges: Array>, - modelRevisionIds: Array<{ modelId: number; revisionId: number }> -): Map>> { - return edges.reduce((map, edge) => { - const edgeRevisionId = edge.properties.revisionId; - const modelRevisionId = modelRevisionIds.find((p) => p.revisionId === edgeRevisionId); - if (modelRevisionId === undefined) return map; - const modelRevisionIdKey: ModelRevisionId = `${modelRevisionId.modelId}-${modelRevisionId.revisionId}`; - const edgesForModel = map.get(modelRevisionIdKey); - if (edgesForModel === undefined) { - map.set(modelRevisionIdKey, [edge]); - } else { - edgesForModel.push(edge); - } - - return map; - }, new Map>>()); -} - -async function getEdgesForRevisions( - revisionIds: number[], - fdmClient: FdmSDK -): Promise>> { - const versionedPropertiesKey = `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`; - const filter = { - in: { - property: [SYSTEM_SPACE_3D_SCHEMA, versionedPropertiesKey, 'revisionId'], - values: revisionIds - } - }; - const mappings = await fdmClient.filterAllInstances( - filter, - 'edge', - SYSTEM_3D_EDGE_SOURCE - ); - return mappings.edges; -} diff --git a/react-components/src/hooks/useNodeMappedData.tsx b/react-components/src/hooks/useNodeMappedData.tsx deleted file mode 100644 index d1afa4dafe7..00000000000 --- a/react-components/src/hooks/useNodeMappedData.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/*! - * Copyright 2023 Cognite AS - */ - -import { useQuery } from '@tanstack/react-query'; - -import { type CogniteCadModel } from '@cognite/reveal'; -import { type CogniteClient, type CogniteInternalId, type Node3D } from '@cognite/sdk'; -import { type NodeDataResult } from '../components/Reveal3DResources/types'; -import { useFdmSdk, useSDK } from '../components/RevealContainer/SDKProvider'; - -import assert from 'assert'; -import { - type FdmSDK, - type DmsUniqueIdentifier, - type EdgeItem, - type InspectResultList -} from '../utilities/FdmSDK'; -import { - INSTANCE_SPACE_3D_DATA, - SYSTEM_3D_EDGE_SOURCE, - SYSTEM_SPACE_3D_SCHEMA -} from '../utilities/globalDataModels'; - -export const useNodeMappedData = ( - treeIndex: number | undefined, - model: CogniteCadModel | undefined -): NodeDataResult | undefined => { - const cogniteClient = useSDK(); - const fdmClient = useFdmSdk(); - - const mappedDataHashKey = `${model?.modelId ?? ''}-${model?.revisionId ?? ''}-${treeIndex ?? ''}`; - - const queryResult = useQuery(['cdf', '3d', mappedDataHashKey], async () => { - if (model === undefined || treeIndex === undefined) { - return null; - } - - const ancestors = await fetchAncestorNodesForTreeIndex(model, treeIndex, cogniteClient); - - if (ancestors.length === 0) { - return null; - } - - const mappings = await fetchNodeMappingEdges( - model, - ancestors.map((n) => n.id), - fdmClient - ); - - const selectedEdge = - mappings !== undefined && mappings.edges.length > 0 ? mappings.edges[0] : undefined; - - const selectedNodeId = selectedEdge?.properties.revisionNodeId; - - const dataNode = selectedEdge?.startNode; - - if (dataNode === undefined) { - return null; - } - - const inspectionResult = await inspectNode(dataNode, fdmClient); - - const dataView = - inspectionResult?.items[0]?.inspectionResults.involvedViewsAndContainers?.views[0]; - - if (dataView === undefined) { - return null; - } - - const selectedNode = ancestors.find((n) => n.id === selectedNodeId); - - assert(selectedNode !== undefined); - - return { - nodeExternalId: dataNode.externalId, - view: dataView, - cadNode: selectedNode - }; - }); - - return queryResult.data ?? undefined; -}; - -async function fetchAncestorNodesForTreeIndex( - model: CogniteCadModel, - treeIndex: number, - cogniteClient: CogniteClient -): Promise { - const nodeId = await model.mapTreeIndexToNodeId(treeIndex); - - const ancestorNodes = await cogniteClient.revisions3D.list3DNodeAncestors( - model.modelId, - model.revisionId, - nodeId - ); - - return ancestorNodes.items; -} - -async function fetchNodeMappingEdges( - model: CogniteCadModel, - ancestorIds: CogniteInternalId[], - fdmClient: FdmSDK -): Promise<{ edges: Array>> } | undefined> { - assert(ancestorIds.length !== 0); - - const filter = { - and: [ - { - equals: { - property: ['edge', 'endNode'], - value: { - space: INSTANCE_SPACE_3D_DATA, - externalId: `${model.modelId}` - } - } - }, - { - equals: { - property: [ - SYSTEM_SPACE_3D_SCHEMA, - `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, - 'revisionId' - ], - value: model.revisionId - } - }, - { - in: { - property: [ - SYSTEM_SPACE_3D_SCHEMA, - `${SYSTEM_3D_EDGE_SOURCE.externalId}/${SYSTEM_3D_EDGE_SOURCE.version}`, - 'revisionNodeId' - ], - values: ancestorIds - } - } - ] - }; - - return await fdmClient.filterAllInstances(filter, 'edge', SYSTEM_3D_EDGE_SOURCE); -} - -async function inspectNode( - dataNode: DmsUniqueIdentifier, - fdmClient: FdmSDK -): Promise { - const inspectionResult = await fdmClient.inspectInstances({ - inspectionOperations: { involvedViewsAndContainers: {} }, - items: [ - { - instanceType: 'node', - externalId: dataNode.externalId, - space: dataNode.space - } - ] - }); - - return inspectionResult; -} diff --git a/react-components/stories/HighlightNode.stories.tsx b/react-components/stories/HighlightNode.stories.tsx index 1f409501d73..5bd142ec153 100644 --- a/react-components/stories/HighlightNode.stories.tsx +++ b/react-components/stories/HighlightNode.stories.tsx @@ -65,12 +65,12 @@ const StoryContent = ({ resources }: { resources: AddResourceOptions[] }): React const cameraNavigation = useCameraNavigation(); useEffect(() => { - setHighlightedId(nodeData?.nodeExternalId); + setHighlightedId(nodeData?.fdmNode.externalId); if (nodeData === undefined) return; - void cameraNavigation.fitCameraToInstance(nodeData.nodeExternalId, 'pdms-mapping'); - }, [nodeData?.nodeExternalId]); + void cameraNavigation.fitCameraToInstance(nodeData.fdmNode.externalId, 'pdms-mapping'); + }, [nodeData?.fdmNode.externalId]); if (stylingGroupsRef.current.length === 1) { stylingGroupsRef.current.pop();