@cornerstonejs/core
Version:
Cornerstone3D Core
527 lines (526 loc) • 22 kB
JavaScript
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;