UNPKG

@cornerstonejs/core

Version:
527 lines (526 loc) • 22 kB
import cache from '../../../cache/cache.js'; import { Events } from '../../../enums/index.js'; import { ActorRenderMode } from '../../../types/index.js'; import triggerEvent from '../../../utilities/triggerEvent.js'; import { findMatchingColormap, getMaxOpacity, getThresholdValue, } from '../../../utilities/colormap.js'; import { getTransferFunctionNodes } from '../../../utilities/transferFunctionUtils.js'; import genericViewportDisplaySetMetadataProvider from '../../../utilities/genericViewportDisplaySetMetadataProvider.js'; import { clonePlanarLegacyProperties, mergePlanarLegacyProperties, clonePlanarOrientation, toPlanarDataPresentation, } from './planarLegacyCompatibility.js'; import { mapSlabTypeToBlendMode } from './planarVolumeSliceBlendMode.js'; export const PLANAR_LEGACY_PER_IMAGE_DEFAULT_PROPERTIES_LIMIT = 2048; class PlanarLegacyCompatibilityController { constructor(host) { this.host = host; this.managedDataIds = new Set(); this.volumeDataIds = new Map(); this.stackSetRequestId = 0; this.properties = new Map(); this.globalDefaultProperties = new Map(); this.perImageIdDefaultProperties = new Map(); this.onStackNewImage = (event) => { if (this.perImageIdDefaultProperties.size === 0) { return; } const detail = event.detail; const imageId = detail?.imageId; const targetDataId = this.resolveTargetDataId(); if (!targetDataId) { return; } const perImageProps = imageId ? this.getPerImageDefaultProperties(imageId) : undefined; const globalProps = this.globalDefaultProperties.get(targetDataId); const propsToApply = perImageProps || globalProps; if (!propsToApply) { return; } this.setProperties(propsToApply); }; host .getElement() .addEventListener(Events.STACK_NEW_IMAGE, this.onStackNewImage); } async setStack(imageIds, currentImageIdIndex = 0) { if (!imageIds.length) { throw new Error('[PlanarViewport] Cannot set an empty stack'); } const requestId = ++this.stackSetRequestId; const dataId = this.getLegacyStackDataId(); const clampedImageIdIndex = Math.min(Math.max(0, currentImageIdIndex), imageIds.length - 1); let registered = false; try { this.host.removeBindingsExcept(new Set()); this.registerDataSet(dataId, { imageIds, initialImageIdIndex: clampedImageIdIndex, }); registered = true; await this.host.setDisplaySets({ displaySetId: dataId, options: { orientation: this.host.getRequestedOrientation(), }, }); if (requestId !== this.stackSetRequestId) { return imageIds[clampedImageIdIndex]; } const resolvedImageId = await this.host.setImageIdIndex(clampedImageIdIndex); this.prunePerImageDefaultProperties(new Set(imageIds)); return this.host.getCurrentImageId() || resolvedImageId; } catch (error) { if (registered && requestId === this.stackSetRequestId) { this.removeData(dataId); } throw error; } } async setVolumes(volumeInputArray, immediate = false, suppressEvents = false) { await this.mountVolumes(volumeInputArray, true, suppressEvents); if (immediate) { this.host.render(); } } async addVolumes(volumeInputArray, immediate = false, suppressEvents = false) { await this.mountVolumes(volumeInputArray, false, suppressEvents); if (immediate) { this.host.render(); } } setProperties(properties = {}, volumeIdOrSuppressEvents, suppressEvents = false) { const volumeId = typeof volumeIdOrSuppressEvents === 'string' ? volumeIdOrSuppressEvents : undefined; const targetDataId = this.resolveTargetDataId(volumeId); if (!targetDataId) { return; } const nextProperties = mergePlanarLegacyProperties(this.properties.get(targetDataId) || {}, properties); const currentDefaults = this.globalDefaultProperties.get(targetDataId) || {}; this.globalDefaultProperties.set(targetDataId, mergePlanarLegacyProperties(properties, currentDefaults)); this.properties.set(targetDataId, nextProperties); if (properties.orientation !== undefined) { this.host.setCameraOrientation(clonePlanarOrientation(properties.orientation)); } this.host.setDataPresentationState(targetDataId, toPlanarDataPresentation(nextProperties)); if (!suppressEvents && properties.colormap !== undefined) { this.emitColormapModified(targetDataId, volumeId); } } setDefaultProperties(properties = {}, imageId) { if (imageId == null) { const targetDataId = this.resolveTargetDataId(); if (!targetDataId) { return; } this.globalDefaultProperties.set(targetDataId, clonePlanarLegacyProperties(properties)); return; } this.setPerImageDefaultProperties(imageId, clonePlanarLegacyProperties(properties)); if (this.host.getCurrentImageId() === imageId) { this.setProperties(properties); } } clearDefaultProperties(imageId) { if (imageId == null) { const targetDataId = this.resolveTargetDataId(); if (targetDataId) { this.globalDefaultProperties.delete(targetDataId); } this.resetProperties(); return; } this.perImageIdDefaultProperties.delete(imageId); this.resetToDefaultProperties(); } resetToDefaultProperties() { const targetDataId = this.resolveTargetDataId(); if (!targetDataId) { return; } const currentImageId = this.host.getCurrentImageId(); const defaultProperties = (currentImageId ? this.getPerImageDefaultProperties(currentImageId) : undefined) || this.globalDefaultProperties.get(targetDataId) || {}; this.properties.set(targetDataId, clonePlanarLegacyProperties(defaultProperties)); if (defaultProperties.orientation !== undefined) { this.host.setCameraOrientation(clonePlanarOrientation(defaultProperties.orientation)); } this.host.setDataPresentationState(targetDataId, toPlanarDataPresentation(defaultProperties)); } getProperties(volumeId) { const targetDataId = this.resolveTargetDataId(volumeId); const dataPresentation = targetDataId ? this.host.getDisplaySetPresentation(targetDataId) : undefined; const legacyProperties = targetDataId ? this.properties.get(targetDataId) : undefined; const merged = { ...clonePlanarLegacyProperties(legacyProperties || {}), ...(dataPresentation ? clonePlanarLegacyProperties(dataPresentation) : {}), ...(this.host.getCameraOrientation() ? { orientation: clonePlanarOrientation(this.host.getCameraOrientation()), } : {}), }; if (!merged.voiRange && targetDataId) { const defaultVOIRange = this.host.getDefaultVOIRange(targetDataId); if (defaultVOIRange) { merged.voiRange = { ...defaultVOIRange }; } } return merged; } resetProperties(volumeId) { const targetDataId = this.resolveTargetDataId(volumeId); if (!targetDataId) { return; } this.properties.set(targetDataId, {}); this.host.setDataPresentationState(targetDataId, {}); } getBlendMode(filterActorUIDs = []) { const targetDataId = this.getBlendModeTargetDataIds(filterActorUIDs)[0]; if (!targetDataId) { return; } const storedBlendMode = this.host.getDisplaySetPresentation(targetDataId)?.blendMode; if (storedBlendMode !== undefined) { return storedBlendMode; } const actor = this.host.getBindingActor(targetDataId); const mapper = actor?.getMapper?.(); const blendMode = mapper?.getBlendMode?.(); if (blendMode !== undefined) { return blendMode; } return mapSlabTypeToBlendMode(mapper?.getSlabType?.()); } setBlendMode(blendMode, filterActorUIDs = [], immediate = false) { const targetDataIds = this.getBlendModeTargetDataIds(filterActorUIDs); if (!targetDataIds.length) { return; } targetDataIds.forEach((dataId) => { const nextProperties = mergePlanarLegacyProperties(this.properties.get(dataId) || {}, { blendMode }); this.properties.set(dataId, nextProperties); this.host.setDisplaySetPresentation(dataId, { blendMode }); }); if (immediate) { this.host.render(); } } getNumberOfSlices() { return Math.max(this.host.getImageCount(), this.host.getMaxImageIdIndex() + 1); } removeData(dataId) { if (!this.managedDataIds.delete(dataId)) { return; } this.unregisterDataId(dataId); } destroy() { let cleanupError; this.host .getElement() .removeEventListener(Events.STACK_NEW_IMAGE, this.onStackNewImage); for (const dataId of Array.from(this.managedDataIds)) { try { this.unregisterDataId(dataId); } catch (error) { cleanupError ??= error; } } this.managedDataIds.clear(); this.volumeDataIds.clear(); this.properties.clear(); this.globalDefaultProperties.clear(); this.perImageIdDefaultProperties.clear(); if (cleanupError !== undefined) { throw cleanupError; } } getLegacyStackDataId() { return `__planar_v2__:${this.host.getViewportId()}:stack`; } getLegacyVolumeDataId(volumeId) { return `__planar_v2__:${this.host.getViewportId()}:volume:${volumeId}`; } registerDataSet(dataId, dataSet) { genericViewportDisplaySetMetadataProvider.add(dataId, dataSet); this.managedDataIds.add(dataId); } async mountVolumes(volumeInputArray, replaceExisting, suppressEvents) { if (!volumeInputArray.length) { return; } const dataIds = []; try { for (const volumeInput of volumeInputArray) { const cachedVolume = cache.getVolume(volumeInput.volumeId); if (!cachedVolume) { throw new Error(`imageVolume with id: ${volumeInput.volumeId} does not exist, you need to create/allocate the volume first`); } const dataId = this.getLegacyVolumeDataId(volumeInput.volumeId); this.registerDataSet(dataId, { imageIds: cachedVolume.imageIds, initialImageIdIndex: this.getInitialVolumeImageIdIndex(cachedVolume.imageIds.length), reference: { kind: 'volume', volumeId: volumeInput.volumeId, }, volumeId: volumeInput.volumeId, }); this.volumeDataIds.set(volumeInput.volumeId, dataId); dataIds.push(dataId); } if (replaceExisting) { this.host.removeBindingsExcept(new Set(dataIds)); } this.host.prepareVolumeCompatibilityCamera(); const sharedOptions = { orientation: this.host.getRequestedOrientation(), }; const existingSourceDataId = this.host.getActiveDataId(); await this.host.setDisplaySets(...dataIds.map((dataId, index) => { const shouldMountAsSource = index === 0 && (replaceExisting || !existingSourceDataId); return { displaySetId: dataId, options: { ...sharedOptions, role: shouldMountAsSource ? 'source' : 'overlay', }, }; })); volumeInputArray.forEach((volumeInput, index) => { const dataId = dataIds[index]; if (volumeInput.visibility !== undefined) { this.host.setDisplaySetPresentation(dataId, { visible: volumeInput.visibility, }); } if (volumeInput.blendMode !== undefined) { const nextProperties = mergePlanarLegacyProperties(this.properties.get(dataId) || {}, { blendMode: volumeInput.blendMode }); this.properties.set(dataId, nextProperties); this.host.setDisplaySetPresentation(dataId, { blendMode: volumeInput.blendMode, }); } if (volumeInput.slabThickness !== undefined) { this.host.setDisplaySetPresentation(dataId, { slabThickness: volumeInput.slabThickness, }); } const actor = this.host.getBindingActor(dataId); if (actor && volumeInput.callback) { volumeInput.callback({ volumeActor: actor, volumeId: volumeInput.volumeId, }); const volumePresentation = this.buildVolumePresentationFromActor(dataId); if (volumePresentation) { this.applyVolumePresentation(dataId, volumePresentation); } } }); if (!suppressEvents) { triggerEvent(this.host.getElement(), Events.VOLUME_VIEWPORT_NEW_VOLUME, { viewportId: this.host.getViewportId(), volumeActors: this.buildVolumeActorEntries(volumeInputArray, dataIds), }); } } catch (error) { dataIds.forEach((dataId) => { this.removeData(dataId); }); throw error; } } getInitialVolumeImageIdIndex(imageCount) { const rendering = this.host.getCurrentPlanarRendering(); if (rendering && (rendering.renderMode === ActorRenderMode.CPU_VOLUME || rendering.renderMode === ActorRenderMode.VTK_VOLUME_SLICE)) { return Math.min(Math.max(0, rendering.currentImageIdIndex), Math.max(imageCount - 1, 0)); } return Math.floor(imageCount / 2); } getBlendModeTargetDataIds(filterActorUIDs) { const volumeDataIds = Array.from(new Set(this.volumeDataIds.values())); if (!filterActorUIDs.length) { return volumeDataIds; } return volumeDataIds.filter((dataId) => { const actor = this.host.getBindingActor(dataId); const actorUid = actor?.getUID?.() || actor?.uid || (actor?.get?.('uid')?.uid ?? undefined); return actorUid ? filterActorUIDs.includes(actorUid) : false; }); } resolveTargetDataId(volumeId) { if (volumeId) { const dataId = this.volumeDataIds.get(volumeId) || this.host.findDataIdByVolumeId(volumeId); if (dataId) { return dataId; } } return this.host.getActiveDataId() || this.host.getFirstBoundDataId(); } emitColormapModified(dataId, requestedVolumeId) { const volumeId = requestedVolumeId || this.resolveVolumeIdForDataId(dataId); const colormap = (volumeId ? this.buildVolumeColormap(dataId) : undefined) || this.properties.get(dataId)?.colormap; if (!colormap) { return; } triggerEvent(this.host.getElement(), Events.COLORMAP_MODIFIED, { viewportId: this.host.getViewportId(), volumeId, colormap, }); } applyVolumePresentation(dataId, volumePresentation) { const nextProperties = mergePlanarLegacyProperties(this.properties.get(dataId) || {}, volumePresentation); this.properties.set(dataId, nextProperties); const currentDefaults = this.globalDefaultProperties.get(dataId) || {}; this.globalDefaultProperties.set(dataId, mergePlanarLegacyProperties(nextProperties, currentDefaults)); this.host.setDataPresentationState(dataId, toPlanarDataPresentation(nextProperties)); } buildVolumeActorEntries(volumeInputArray, dataIds) { return volumeInputArray.flatMap((volumeInput, index) => { const actor = this.host.getBindingActor(dataIds[index]); if (!actor) { return []; } const { actorUID, slabThickness, volumeId, ...rest } = volumeInput; return [ { uid: actorUID || volumeId, actor, slabThickness, referencedId: volumeId, ...rest, }, ]; }); } buildVolumeColormap(dataId) { const actor = this.host.getBindingActor(dataId); const transferFunction = actor?.getProperty?.().getRGBTransferFunction?.(0); if (!actor || !transferFunction) { return; } const rgbPoints = getTransferFunctionNodes(transferFunction).reduce((points, node) => { points.push(node[0], node[1], node[2], node[3]); return points; }, []); const matchedColormap = findMatchingColormap(rgbPoints, actor) || {}; const threshold = getThresholdValue(actor); if (matchedColormap.opacity === undefined) { matchedColormap.opacity = getMaxOpacity(actor); } if (threshold !== null) { matchedColormap.threshold = threshold; } return Object.keys(matchedColormap).length ? matchedColormap : undefined; } buildVolumePresentationFromActor(dataId) { const actor = this.host.getBindingActor(dataId); const transferFunction = actor?.getProperty?.().getRGBTransferFunction?.(0); const volumeColormap = this.buildVolumeColormap(dataId); const mapper = actor?.getMapper?.(); const blendMode = mapper?.getBlendMode?.() ?? mapSlabTypeToBlendMode(mapper?.getSlabType?.()); const range = transferFunction?.getRange?.(); const volumePresentation = {}; if (volumeColormap) { volumePresentation.colormap = volumeColormap; } if (range && range[1] > range[0]) { volumePresentation.voiRange = { lower: range[0], upper: range[1], }; } if (blendMode !== undefined) { volumePresentation.blendMode = blendMode; } return Object.keys(volumePresentation).length ? volumePresentation : undefined; } resolveVolumeIdForDataId(dataId) { for (const [volumeId, mappedDataId] of this.volumeDataIds.entries()) { if (mappedDataId === dataId) { return volumeId; } } } getPerImageDefaultProperties(imageId) { const properties = this.perImageIdDefaultProperties.get(imageId); if (properties) { this.perImageIdDefaultProperties.delete(imageId); this.perImageIdDefaultProperties.set(imageId, properties); } return properties; } setPerImageDefaultProperties(imageId, properties) { if (this.perImageIdDefaultProperties.has(imageId)) { this.perImageIdDefaultProperties.delete(imageId); } this.perImageIdDefaultProperties.set(imageId, properties); this.evictPerImageDefaultProperties(); } prunePerImageDefaultProperties(validImageIds) { for (const imageId of this.perImageIdDefaultProperties.keys()) { if (!validImageIds.has(imageId)) { this.perImageIdDefaultProperties.delete(imageId); } } this.evictPerImageDefaultProperties(); } evictPerImageDefaultProperties() { while (this.perImageIdDefaultProperties.size > PLANAR_LEGACY_PER_IMAGE_DEFAULT_PROPERTIES_LIMIT) { const oldestImageId = this.perImageIdDefaultProperties .keys() .next().value; if (oldestImageId === undefined) { return; } this.perImageIdDefaultProperties.delete(oldestImageId); } } unregisterDataId(dataId) { try { genericViewportDisplaySetMetadataProvider.remove(dataId); } finally { this.properties.delete(dataId); this.globalDefaultProperties.delete(dataId); this.removeVolumeDataId(dataId); } } removeVolumeDataId(dataId) { for (const [volumeId, mappedDataId] of this.volumeDataIds.entries()) { if (mappedDataId === dataId) { this.volumeDataIds.delete(volumeId); } } } } export default PlanarLegacyCompatibilityController;