From 1881ce5cbc0a6065a97394a49f467f55ccf28c9c Mon Sep 17 00:00:00 2001 From: Alireza Date: Tue, 16 May 2023 13:07:37 -0400 Subject: [PATCH] feat(custom displayOptions): add custom options to every level of hp (#3372) * feat(custom displayOptions): add custom options to every level of hanging protocols * move set preset down to cornerstone3d * works * fix tmtv based on new api * fix unit test * close * review1 * upgrade packages * update packages * apply review comments * recursive call * less copy * apply review comments * fix e2e tests * apply review comments * revert * apply review comments * apply review comments --------- Co-authored-by: Bill Wallace --- extensions/cornerstone/package.json | 4 +- extensions/cornerstone/src/commandsModule.ts | 5 +- .../DicomUpload/DicomUploadProgress.tsx | 2 +- .../src/getHangingProtocolModule.ts | 2 +- extensions/cornerstone/src/index.tsx | 1 - .../CornerstoneViewportService.ts | 93 ++-- .../src/services/ViewportService/Viewport.ts | 6 +- .../src/utils/colormap/applyPreset.js | 139 ------ .../cornerstone/src/utils/colormap/colors.js | 29 -- .../cornerstone/src/utils/colormap/presets.js | 435 ------------------ .../utils/colormap/transferFunctionHelpers.js | 93 ---- extensions/tmtv/src/init.js | 13 +- extensions/tmtv/src/utils/hpViewports.ts | 36 +- modes/tmtv/src/index.js | 41 +- platform/core/package.json | 2 +- .../HangingProtocolService.ts | 168 ++++--- .../platform/extensions/modules/hpModule.md | 3 +- .../services/data/HangingProtocolService.md | 2 + .../viewer/src/components/ViewportGrid.tsx | 17 +- 19 files changed, 240 insertions(+), 851 deletions(-) delete mode 100644 extensions/cornerstone/src/utils/colormap/applyPreset.js delete mode 100644 extensions/cornerstone/src/utils/colormap/colors.js delete mode 100644 extensions/cornerstone/src/utils/colormap/presets.js delete mode 100644 extensions/cornerstone/src/utils/colormap/transferFunctionHelpers.js diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index c22365ed9..e4954986e 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -39,8 +39,8 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "dcmjs": "^0.29.5", - "dicom-parser": "^1.8.9", + "dcmjs": "^0.29.6", + "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", "react": "^17.0.2", diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index beb45115d..e11412f05 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -14,7 +14,6 @@ import { Types as OhifTypes } from '@ohif/core'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import callInputDialog from './utils/callInputDialog'; -import { setColormap } from './utils/colormap/transferFunctionHelpers'; import toggleStackImageSync from './utils/stackSync/toggleStackImageSync'; import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; @@ -556,9 +555,9 @@ function commandsModule({ return actorEntry.uid.includes(displaySetInstanceUID); }); - const { actor: volumeActor } = actorEntry; + const { actor: volumeActor, uid: volumeId } = actorEntry; - setColormap(volumeActor, colormap); + viewport.setProperties({ colormap, volumeActor }, volumeId); if (immediate) { viewport.render(); diff --git a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx index f1b2de731..d16ee4b9f 100644 --- a/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx +++ b/extensions/cornerstone/src/components/DicomUpload/DicomUploadProgress.tsx @@ -315,7 +315,7 @@ function DicomUploadProgress({ className="ml-auto" onClick={onComplete} > - {'Open Viewer'} + {'Close'} ) : ( diff --git a/extensions/cornerstone/src/getHangingProtocolModule.ts b/extensions/cornerstone/src/getHangingProtocolModule.ts index 985c60ea6..15ff51460 100644 --- a/extensions/cornerstone/src/getHangingProtocolModule.ts +++ b/extensions/cornerstone/src/getHangingProtocolModule.ts @@ -245,7 +245,7 @@ const mprAnd3DVolumeViewport = { { id: 'mprDisplaySet', options: { - presetName: 'CT-Bone', + displayPreset: 'CT-Bone', }, }, ], diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index d213ac77f..142348a9b 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -121,7 +121,6 @@ const cornerstoneExtension: Types.Extensions.Extension = { }, getEnabledElement, dicomLoaderService, - registerColormap, }, }, { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 012c69e6a..d20ef2f76 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -30,10 +30,6 @@ import { VolumeViewportData, } from '../../types/CornerstoneCacheService'; import { Presentation, Presentations } from '../../types/Presentation'; -import { - setColormap, - setLowerUpperColorTransferFunction, -} from '../../utils/colormap/transferFunctionHelpers'; import JumpPresets from '../../utils/JumpPresets'; @@ -525,16 +521,9 @@ class CornerstoneViewportService extends PubSubService const displaySetOptions = displaySetOptionsArray[index]; const { volumeId } = volume; - const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions); - - const callback = ({ volumeActor }) => { - voiCallbacks.forEach(callback => callback(volumeActor)); - }; - volumeInputArray.push({ imageIds, volumeId, - callback, blendMode: displaySetOptions.blendMode, slabThickness: this._getSlabThickness(displaySetOptions, volumeId), }); @@ -577,7 +566,44 @@ class CornerstoneViewportService extends PubSubService toolGroupService, } = this.servicesManager.services; + const viewportInfo = this.getViewportInfo(viewport.id); + const displaySetOptions = viewportInfo.getDisplaySetOptions(); + + // Todo: use presentations states + const volumesProperties = volumeInputArray.map((volumeInput, index) => { + const { volumeId } = volumeInput; + const displaySetOption = displaySetOptions[index]; + const { voi, voiInverted, colormap, displayPreset } = displaySetOption; + const properties = {}; + + if (voi && (voi.windowWidth || voi.windowCenter)) { + const { lower, upper } = csUtils.windowLevel.toLowHighRange( + voi.windowWidth, + voi.windowCenter + ); + properties.voiRange = { lower, upper }; + } + + if (voiInverted !== undefined) { + properties.invert = voiInverted; + } + + if (colormap !== undefined) { + properties.colormap = colormap; + } + + if (displayPreset !== undefined) { + properties.preset = displayPreset; + } + + return { properties, volumeId }; + }); + await viewport.setVolumes(volumeInputArray); + volumesProperties.forEach(({ properties, volumeId }) => { + viewport.setProperties(properties, volumeId); + }); + this.setPresentations(viewport, presentations); // load any secondary displaySets @@ -602,7 +628,6 @@ class CornerstoneViewportService extends PubSubService ); } - const viewportInfo = this.getViewportInfo(viewport.id); const toolGroup = toolGroupService.getToolGroupForViewport(viewport.id); csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id); @@ -737,50 +762,6 @@ class CornerstoneViewportService extends PubSubService } } - _getVOICallbacks(volumeId, displaySetOptions) { - const { - voi, - voiInverted: inverted, - colormap, - presetName, - } = displaySetOptions; - - const voiCallbackArray = []; - - // If colormap is set, use it to set the color transfer function - if (colormap) { - voiCallbackArray.push(volumeActor => setColormap(volumeActor, colormap)); - } - - if (voi instanceof Object && voi.windowWidth && voi.windowCenter) { - const { windowWidth, windowCenter } = voi; - const { lower, upper } = csUtils.windowLevel.toLowHighRange( - windowWidth, - windowCenter - ); - voiCallbackArray.push(volumeActor => - setLowerUpperColorTransferFunction({ - volumeActor, - lower, - upper, - inverted, - }) - ); - } - - if (presetName) { - voiCallbackArray.push(volumeActor => { - utilities.applyPreset( - volumeActor, - CONSTANTS.VIEWPORT_PRESETS.find(preset => { - return preset.name === presetName; - }) - ); - }); - } - return voiCallbackArray; - } - _setDisplaySets( viewport: StackViewport | VolumeViewport, viewportData: StackViewportData | VolumeViewportData, diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index e5b36342f..1f5247bb1 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -63,7 +63,7 @@ export type PublicDisplaySetOptions = { blendMode?: string; slabThickness?: number; colormap?: string; - presetName?: string; + displayPreset?: string; }; export type DisplaySetOptions = { @@ -73,7 +73,7 @@ export type DisplaySetOptions = { blendMode?: Enums.BlendModes; slabThickness?: number; colormap?: string; - presetName?: string; + displayPreset?: string; }; type VOI = { @@ -315,7 +315,7 @@ class ViewportInfo { colormap: option.colormap, slabThickness: option.slabThickness, blendMode, - presetName: option.presetName, + displayPreset: option.displayPreset, }); }); diff --git a/extensions/cornerstone/src/utils/colormap/applyPreset.js b/extensions/cornerstone/src/utils/colormap/applyPreset.js deleted file mode 100644 index 087ce4857..000000000 --- a/extensions/cornerstone/src/utils/colormap/applyPreset.js +++ /dev/null @@ -1,139 +0,0 @@ -import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; -import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; - -import presets from './presets'; - -// get preset from preset name -function getPreset(name) { - return presets.find(preset => preset.name === name); -} - -// Todo: we should be able to register a new preset via the utilityModule -// of the cornerstone extension -export default function applyPreset(actor, presetName) { - const preset = getPreset(presetName); - // Create color transfer function - const colorTransferArray = preset.colorTransfer - .split(' ') - .splice(1) - .map(parseFloat); - - const { shiftRange } = getShiftRange(colorTransferArray); - let min = shiftRange[0]; - const width = shiftRange[1] - shiftRange[0]; - const cfun = vtkColorTransferFunction.newInstance(); - const normColorTransferValuePoints = []; - for (let i = 0; i < colorTransferArray.length; i += 4) { - let value = colorTransferArray[i]; - const r = colorTransferArray[i + 1]; - const g = colorTransferArray[i + 2]; - const b = colorTransferArray[i + 3]; - - value = (value - min) / width; - normColorTransferValuePoints.push([value, r, g, b]); - } - - applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun); - - actor.getProperty().setRGBTransferFunction(0, cfun); - - // Create scalar opacity function - const scalarOpacityArray = preset.scalarOpacity - .split(' ') - .splice(1) - .map(parseFloat); - - const ofun = vtkPiecewiseFunction.newInstance(); - const normPoints = []; - for (let i = 0; i < scalarOpacityArray.length; i += 2) { - let value = scalarOpacityArray[i]; - const opacity = scalarOpacityArray[i + 1]; - - value = (value - min) / width; - - normPoints.push([value, opacity]); - } - - applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun); - - actor.getProperty().setScalarOpacity(0, ofun); - - const [ - gradientMinValue, - gradientMinOpacity, - gradientMaxValue, - gradientMaxOpacity, - ] = preset.gradientOpacity - .split(' ') - .splice(1) - .map(parseFloat); - - actor.getProperty().setUseGradientOpacity(0, true); - actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue); - actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity); - actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue); - actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity); - - if (preset.interpolation === '1') { - actor.getProperty().setInterpolationTypeToFastLinear(); - //actor.getProperty().setInterpolationTypeToLinear() - } - - const ambient = parseFloat(preset.ambient); - //const shade = preset.shade === '1' - const diffuse = parseFloat(preset.diffuse); - const specular = parseFloat(preset.specular); - const specularPower = parseFloat(preset.specularPower); - - //actor.getProperty().setShade(shade) - actor.getProperty().setAmbient(ambient); - actor.getProperty().setDiffuse(diffuse); - actor.getProperty().setSpecular(specular); - actor.getProperty().setSpecularPower(specularPower); -} - -function getShiftRange(colorTransferArray) { - // Credit to paraview-glance - // https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133 - - // shift range is original rgb/opacity range centered around 0 - let min = Infinity; - let max = -Infinity; - for (let i = 0; i < colorTransferArray.length; i += 4) { - min = Math.min(min, colorTransferArray[i]); - max = Math.max(max, colorTransferArray[i]); - } - - const center = (max - min) / 2; - - return { - shiftRange: [-center, center], - min, - max, - }; -} - -function applyPointsToRGBFunction(points, range, cfun) { - const width = range[1] - range[0]; - const rescaled = points.map(([x, r, g, b]) => [ - x * width + range[0], - r, - g, - b, - ]); - - cfun.removeAllPoints(); - rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b)); - - return rescaled; -} - -function applyPointsToPiecewiseFunction(points, range, pwf) { - const width = range[1] - range[0]; - const rescaled = points.map(([x, y]) => [x * width + range[0], y]); - - pwf.removeAllPoints(); - rescaled.forEach(([x, y]) => pwf.addPoint(x, y)); - - return rescaled; -} diff --git a/extensions/cornerstone/src/utils/colormap/colors.js b/extensions/cornerstone/src/utils/colormap/colors.js deleted file mode 100644 index 2cc80a9a2..000000000 --- a/extensions/cornerstone/src/utils/colormap/colors.js +++ /dev/null @@ -1,29 +0,0 @@ -// https://www.slicer.org/w/index.php/Slicer3:2010_GenericAnatomyColors#Lookup_table -const colors = [ - { - integerLabel: 0, - textLabel: 'background', - color: [0, 0, 0, 0], - }, - { - integerLabel: 1, - textLabel: 'tissue', - // color: [255, 174, 128, 255], - color: [255, 0, 0, 255], - }, - { - integerLabel: 2, - textLabel: 'bone', - // color: [241, 214, 145, 255], - color: [0, 255, 0, 255], - }, - { - integerLabel: 3, - textLabel: 'skin', - // color: [177, 122, 101, 255], - color: [0, 0, 255, 255], - }, - // .... -]; - -export default colors; diff --git a/extensions/cornerstone/src/utils/colormap/presets.js b/extensions/cornerstone/src/utils/colormap/presets.js deleted file mode 100644 index 4db2da8e3..000000000 --- a/extensions/cornerstone/src/utils/colormap/presets.js +++ /dev/null @@ -1,435 +0,0 @@ -const presets = [ - { - name: 'CT-AAA', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '12 -3024 0 143.556 0 166.222 0.686275 214.389 0.696078 419.736 0.833333 3071 0.803922', - id: 'vtkMRMLVolumePropertyNode1', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '24 -3024 0 0 0 143.556 0.615686 0.356863 0.184314 166.222 0.882353 0.603922 0.290196 214.389 1 1 1 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '143.556 419.736', - }, - { - name: 'CT-AAA2', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '16 -3024 0 129.542 0 145.244 0.166667 157.02 0.5 169.918 0.627451 395.575 0.8125 1578.73 0.8125 3071 0.8125', - id: 'vtkMRMLVolumePropertyNode2', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '32 -3024 0 0 0 129.542 0.54902 0.25098 0.14902 145.244 0.6 0.627451 0.843137 157.02 0.890196 0.47451 0.6 169.918 0.992157 0.870588 0.392157 395.575 1 0.886275 0.658824 1578.73 1 0.829256 0.957922 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '0 1600', - }, - { - name: 'CT-Bone', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: '8 -3024 0 -16.4458 0 641.385 0.715686 3071 0.705882', - id: 'vtkMRMLVolumePropertyNode3', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '16 -3024 0 0 0 -16.4458 0.729412 0.254902 0.301961 641.385 0.905882 0.815686 0.552941 3071 1 1 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '-16.4458 641.385', - }, - { - name: 'CT-Bones', - gradientOpacity: '4 0 1 985.12 1', - specularPower: '1', - scalarOpacity: '8 -1000 0 152.19 0 278.93 0.190476 952 0.2', - id: 'vtkMRMLVolumePropertyNode4', - specular: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '20 -1000 0.3 0.3 1 -488 0.3 1 0.3 463.28 1 0 0 659.15 1 0.912535 0.0374849 953 1 0.3 0.3', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '152.19 952', - }, - { - name: 'CT-Cardiac', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '12 -3024 0 -77.6875 0 94.9518 0.285714 179.052 0.553571 260.439 0.848214 3071 0.875', - id: 'vtkMRMLVolumePropertyNode5', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '24 -3024 0 0 0 -77.6875 0.54902 0.25098 0.14902 94.9518 0.882353 0.603922 0.290196 179.052 1 0.937033 0.954531 260.439 0.615686 0 0 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '-77.6875 260.439', - }, - { - name: 'CT-Cardiac2', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '12 -3024 0 42.8964 0 163.488 0.428571 277.642 0.776786 1587 0.754902 3071 0.754902', - id: 'vtkMRMLVolumePropertyNode6', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '24 -3024 0 0 0 42.8964 0.54902 0.25098 0.14902 163.488 0.917647 0.639216 0.0588235 277.642 1 0.878431 0.623529 1587 1 1 1 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '42.8964 1587', - }, - { - name: 'CT-Cardiac3', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '14 -3024 0 -86.9767 0 45.3791 0.169643 139.919 0.589286 347.907 0.607143 1224.16 0.607143 3071 0.616071', - id: 'vtkMRMLVolumePropertyNode7', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '28 -3024 0 0 0 -86.9767 0 0.25098 1 45.3791 1 0 0 139.919 1 0.894893 0.894893 347.907 1 1 0.25098 1224.16 1 1 1 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '-86.9767 1224.16', - }, - { - name: 'CT-Chest-Contrast-Enhanced', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '10 -3024 0 67.0106 0 251.105 0.446429 439.291 0.625 3071 0.616071', - id: 'vtkMRMLVolumePropertyNode8', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '20 -3024 0 0 0 67.0106 0.54902 0.25098 0.14902 251.105 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '67.0106 439.291', - }, - { - name: 'CT-Chest-Vessels', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: - '10 -3024 0 -1278.35 0 22.8277 0.428571 439.291 0.625 3071 0.616071', - id: 'vtkMRMLVolumePropertyNode9', - specular: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '20 -3024 0 0 0 -1278.35 0.54902 0.25098 0.14902 22.8277 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '-1278.35 439.291', - }, - { - name: 'CT-Coronary-Arteries', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: - '12 -2048 0 136.47 0 159.215 0.258929 318.43 0.571429 478.693 0.776786 3661 1', - id: 'vtkMRMLVolumePropertyNode10', - specular: '0', - shade: '0', - ambient: '0.2', - colorTransfer: - '24 -2048 0 0 0 136.47 0 0 0 159.215 0.159804 0.159804 0.159804 318.43 0.764706 0.764706 0.764706 478.693 1 1 1 3661 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '136.47 478.693', - }, - { - name: 'CT-Coronary-Arteries-2', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: - '14 -2048 0 142.677 0 145.016 0.116071 192.174 0.5625 217.24 0.776786 384.347 0.830357 3661 0.830357', - id: 'vtkMRMLVolumePropertyNode11', - specular: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '28 -2048 0 0 0 142.677 0 0 0 145.016 0.615686 0 0.0156863 192.174 0.909804 0.454902 0 217.24 0.972549 0.807843 0.611765 384.347 0.909804 0.909804 1 3661 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '142.677 384.347', - }, - { - name: 'CT-Coronary-Arteries-3', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '14 -2048 0 128.643 0 129.982 0.0982143 173.636 0.669643 255.884 0.857143 584.878 0.866071 3661 1', - id: 'vtkMRMLVolumePropertyNode12', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '28 -2048 0 0 0 128.643 0 0 0 129.982 0.615686 0 0.0156863 173.636 0.909804 0.454902 0 255.884 0.886275 0.886275 0.886275 584.878 0.968627 0.968627 0.968627 3661 1 1 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '128.643 584.878', - }, - { - name: 'CT-Cropped-Volume-Bone', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: '10 -2048 0 -451 0 -450 1 1050 1 3661 1', - id: 'vtkMRMLVolumePropertyNode13', - specular: '0', - shade: '0', - ambient: '0.2', - colorTransfer: - '20 -2048 0 0 0 -451 0 0 0 -450 0.0556356 0.0556356 0.0556356 1050 1 1 1 3661 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '-451 1050', - }, - { - name: 'CT-Fat', - gradientOpacity: '6 0 1 985.12 1 988 1', - specularPower: '1', - scalarOpacity: '14 -1000 0 -100 0 -99 0.15 -60 0.15 -59 0 101.2 0 952 0', - id: 'vtkMRMLVolumePropertyNode14', - specular: '0', - references: '0', - shade: '0', - ambient: '0.2', - colorTransfer: - '36 -1000 0.3 0.3 1 -497.5 0.3 1 0.3 -99 0 0 1 -76.946 0 1 0 -65.481 0.835431 0.888889 0.0165387 83.89 1 0 0 463.28 1 0 0 659.15 1 0.912535 0.0374849 2952 1 0.300267 0.299886', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '-100 101.2', - }, - { - name: 'CT-Liver-Vasculature', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: - '14 -2048 0 149.113 0 157.884 0.482143 339.96 0.660714 388.526 0.830357 1197.95 0.839286 3661 0.848214', - id: 'vtkMRMLVolumePropertyNode15', - specular: '0', - shade: '0', - ambient: '0.2', - colorTransfer: - '28 -2048 0 0 0 149.113 0 0 0 157.884 0.501961 0.25098 0 339.96 0.695386 0.59603 0.36886 388.526 0.854902 0.85098 0.827451 1197.95 1 1 1 3661 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '149.113 1197.95', - }, - { - name: 'CT-Lung', - gradientOpacity: '6 0 1 985.12 1 988 1', - specularPower: '1', - scalarOpacity: '12 -1000 0 -600 0 -599 0.15 -400 0.15 -399 0 2952 0', - id: 'vtkMRMLVolumePropertyNode16', - specular: '0', - references: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '24 -1000 0.3 0.3 1 -600 0 0 1 -530 0.134704 0.781726 0.0724558 -460 0.929244 1 0.109473 -400 0.888889 0.254949 0.0240258 2952 1 0.3 0.3', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '-600 -399', - }, - { - name: 'CT-MIP', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: '8 -3024 0 -637.62 0 700 1 3071 1', - id: 'vtkMRMLVolumePropertyNode17', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: '16 -3024 0 0 0 -637.62 1 1 1 700 1 1 1 3071 1 1 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '-637.62 700', - }, - { - name: 'CT-Muscle', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: - '10 -3024 0 -155.407 0 217.641 0.676471 419.736 0.833333 3071 0.803922', - id: 'vtkMRMLVolumePropertyNode18', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '20 -3024 0 0 0 -155.407 0.54902 0.25098 0.14902 217.641 0.882353 0.603922 0.290196 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '-155.407 419.736', - }, - { - name: 'CT-Pulmonary-Arteries', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: - '14 -2048 0 -568.625 0 -364.081 0.0714286 -244.813 0.401786 18.2775 0.607143 447.798 0.830357 3592.73 0.839286', - id: 'vtkMRMLVolumePropertyNode19', - specular: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '28 -2048 0 0 0 -568.625 0 0 0 -364.081 0.396078 0.301961 0.180392 -244.813 0.611765 0.352941 0.0705882 18.2775 0.843137 0.0156863 0.156863 447.798 0.752941 0.752941 0.752941 3592.73 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '-568.625 447.798', - }, - { - name: 'CT-Soft-Tissue', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: '10 -2048 0 -167.01 0 -160 1 240 1 3661 1', - id: 'vtkMRMLVolumePropertyNode20', - specular: '0', - shade: '0', - ambient: '0.2', - colorTransfer: - '20 -2048 0 0 0 -167.01 0 0 0 -160 0.0556356 0.0556356 0.0556356 240 1 1 1 3661 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '-167.01 240', - }, - { - name: 'CT-Air', - gradientOpacity: '4 0 1 255 1', - specularPower: '10', - scalarOpacity: '8 -3024 0.705882 -900.0 0.715686 -500.0 0 3071 0', - id: 'vtkMRMLVolumePropertyNode21', - specular: '0.2', - shade: '1', - ambient: '0.1', - colorTransfer: - '16 -3024 1 1 1 -900.0 0.2 1.0 1.0 -500.0 0.3 0.3 1.0 3071 0 0 0 ', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '-1200.0 -200.0', - }, - { - name: 'MR-Angio', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: - '12 -2048 0 151.354 0 158.279 0.4375 190.112 0.580357 200.873 0.732143 3661 0.741071', - id: 'vtkMRMLVolumePropertyNode22', - specular: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '24 -2048 0 0 0 151.354 0 0 0 158.279 0.74902 0.376471 0 190.112 1 0.866667 0.733333 200.873 0.937255 0.937255 0.937255 3661 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '151.354 200.873', - }, - { - name: 'MR-Default', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: '12 0 0 20 0 40 0.15 120 0.3 220 0.375 1024 0.5', - id: 'vtkMRMLVolumePropertyNode23', - specular: '0', - shade: '1', - ambient: '0.2', - colorTransfer: - '24 0 0 0 0 20 0.168627 0 0 40 0.403922 0.145098 0.0784314 120 0.780392 0.607843 0.380392 220 0.847059 0.835294 0.788235 1024 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '0 220', - }, - { - name: 'MR-MIP', - gradientOpacity: '4 0 1 255 1', - specularPower: '1', - scalarOpacity: '8 0 0 98.3725 0 416.637 1 2800 1', - id: 'vtkMRMLVolumePropertyNode24', - specular: '0', - shade: '0', - ambient: '0.2', - colorTransfer: '16 0 1 1 1 98.3725 1 1 1 416.637 1 1 1 2800 1 1 1', - selectable: 'true', - diffuse: '1', - interpolation: '1', - effectiveRange: '0 416.637', - }, - { - name: 'MR-T2-Brain', - gradientOpacity: '4 0 1 160.25 1', - specularPower: '40', - scalarOpacity: '10 0 0 36.05 0 218.302 0.171429 412.406 1 641 1', - id: 'vtkMRMLVolumePropertyNode25', - specular: '0.5', - shade: '1', - ambient: '0.3', - colorTransfer: - '16 0 0 0 0 98.7223 0.956863 0.839216 0.192157 412.406 0 0.592157 0.807843 641 1 1 1', - selectable: 'true', - diffuse: '0.6', - interpolation: '1', - effectiveRange: '0 412.406', - }, - { - name: 'DTI-FA-Brain', - gradientOpacity: '4 0 1 0.9950 1', - specularPower: '40', - scalarOpacity: - '16 0 0 0 0 0.3501 0.0158 0.49379 0.7619 0.6419 1 0.9920 1 0.9950 0 0.9950 0', - id: 'vtkMRMLVolumePropertyNode26', - specular: '0.5', - shade: '1', - ambient: '0.3', - colorTransfer: - '28 0 1 0 0 0 1 0 0 0.24974 0.4941 1 0 0.49949 0 0.9882 1 0.7492 0.51764 0 1 0.9950 1 0 0 0.9950 1 0 0', - selectable: 'true', - diffuse: '0.9', - interpolation: '1', - effectiveRange: '0 1', - }, -]; - -export default presets; diff --git a/extensions/cornerstone/src/utils/colormap/transferFunctionHelpers.js b/extensions/cornerstone/src/utils/colormap/transferFunctionHelpers.js deleted file mode 100644 index 4bbc25713..000000000 --- a/extensions/cornerstone/src/utils/colormap/transferFunctionHelpers.js +++ /dev/null @@ -1,93 +0,0 @@ -import { cache, utilities } from '@cornerstonejs/core'; -import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps'; -import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction'; -import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction'; - -const colormaps = {}; - -export function registerColormap(colormap) { - colormaps[colormap.Name] = colormap; -} - -function setColorTransferFunctionFromVolumeMetadata({ - volumeActor, - volumeId, - inverted, -}) { - let lower, upper, windowWidth, windowCenter; - - if (volumeId) { - const volume = cache.getVolume(volumeId); - const voiLutModule = volume.metadata.voiLut[0]; - if (voiLutModule) { - windowWidth = voiLutModule.windowWidth; - windowCenter = voiLutModule.windowCenter; - } - } else { - windowWidth = 400; - windowCenter = 40; - } - - if (windowWidth == undefined || windowCenter === undefined) { - // Set to something so we can window level it manually. - lower = 200; - upper = 400; - } else { - lower = windowCenter - windowWidth / 2.0; - upper = windowCenter + windowWidth / 2.0; - } - - setLowerUpperColorTransferFunction({ volumeActor, lower, upper, inverted }); -} - -function setLowerUpperColorTransferFunction({ - volumeActor, - lower, - upper, - inverted, -}) { - volumeActor - .getProperty() - .getRGBTransferFunction(0) - .setMappingRange(lower, upper); - - if (inverted) { - utilities.invertRgbTransferFunction( - volumeActor.getProperty().getRGBTransferFunction(0) - ); - } -} - -function setColormap(volumeActor, colormap) { - const mapper = volumeActor.getMapper(); - mapper.setSampleDistance(1.0); - - const cfun = vtkColorTransferFunction.newInstance(); - - // if we have a custom colormap, use it - let preset; - if (colormaps[colormap]) { - preset = colormaps[colormap]; - } else { - preset = vtkColorMaps.getPresetByName(colormap); - } - - cfun.applyColorMap(preset); - cfun.setMappingRange(0, 5); - - volumeActor.getProperty().setRGBTransferFunction(0, cfun); - - // Create scalar opacity function - const ofun = vtkPiecewiseFunction.newInstance(); - ofun.addPoint(0, 0.0); - ofun.addPoint(0.1, 0.9); - ofun.addPoint(5, 1.0); - - volumeActor.getProperty().setScalarOpacity(0, ofun); -} - -export { - setColormap, - setColorTransferFunctionFromVolumeMetadata, - setLowerUpperColorTransferFunction, -}; diff --git a/extensions/tmtv/src/init.js b/extensions/tmtv/src/init.js index 3c694cf37..221fac514 100644 --- a/extensions/tmtv/src/init.js +++ b/extensions/tmtv/src/init.js @@ -2,10 +2,13 @@ import { addTool, RectangleROIStartEndThresholdTool, } from '@cornerstonejs/tools'; +import { utilities } from '@cornerstonejs/core'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; import colormaps from './utils/colormaps'; +const { registerColormap } = utilities.colormap; + const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools'; const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1'; /** @@ -42,15 +45,5 @@ export default function init({ servicesManager, extensionManager }) { RectangleROIStartEndThreshold.toMeasurement ); - initColormaps(extensionManager); -} - -function initColormaps(extensionManager) { - const utilityModule = extensionManager.getModuleEntry( - '@ohif/extension-cornerstone.utilityModule.common' - ); - - const { registerColormap } = utilityModule.exports; - colormaps.forEach(registerColormap); } diff --git a/extensions/tmtv/src/utils/hpViewports.ts b/extensions/tmtv/src/utils/hpViewports.ts index 0efc354ec..af2b760d4 100644 --- a/extensions/tmtv/src/utils/hpViewports.ts +++ b/extensions/tmtv/src/utils/hpViewports.ts @@ -121,8 +121,7 @@ const ptAXIAL = { { options: { voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, voiInverted: true, }, @@ -163,8 +162,7 @@ const ptSAGITTAL = { { options: { voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, voiInverted: true, }, @@ -205,8 +203,7 @@ const ptCORONAL = { { options: { voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, voiInverted: true, }, @@ -258,10 +255,12 @@ const fusionAXIAL = { }, { options: { - colormap: 'hsv', + colormap: { + name: 'hsv', + opacityMapping: [{ value: 0.1, opacity: 0.9 }], + }, voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, }, id: 'ptDisplaySet', @@ -312,10 +311,12 @@ const fusionSAGITTAL = { }, { options: { - colormap: 'hsv', + colormap: { + name: 'hsv', + opacityMapping: [{ value: 0.1, opacity: 0.9 }], + }, voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, }, id: 'ptDisplaySet', @@ -366,10 +367,12 @@ const fusionCORONAL = { }, { options: { - colormap: 'hsv', + colormap: { + name: 'hsv', + opacityMapping: [{ value: 0.1, opacity: 0.9 }], + }, voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, }, id: 'ptDisplaySet', @@ -414,8 +417,7 @@ const mipSAGITTAL = { blendMode: 'MIP', slabThickness: 'fullVolume', voi: { - windowWidth: 5, - windowCenter: 2.5, + custom: 'getPTVOIRange', }, voiInverted: true, }, diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js index c0f8659ae..e4cfc5c25 100644 --- a/modes/tmtv/src/index.js +++ b/modes/tmtv/src/index.js @@ -1,10 +1,12 @@ -import { hotkeys } from '@ohif/core'; +import { hotkeys, classes } from '@ohif/core'; import toolbarButtons from './toolbarButtons.js'; import { id } from './id.js'; import initToolGroups, { toolGroupIds } from './initToolGroups.js'; import setCrosshairsConfiguration from './utils/setCrosshairsConfiguration.js'; import setFusionActiveVolume from './utils/setFusionActiveVolume.js'; +const { MetadataProvider } = classes; + const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', @@ -131,6 +133,43 @@ function modeFactory({ modeConfiguration }) { 'RectangleROIStartEndThreshold', 'fusionPTColormap', ]); + + // For the hanging protocol we need to decide on the window level + // based on whether the SUV is corrected or not, hence we can't hard + // code the window level in the hanging protocol but we add a custom + // attribute to the hanging protocol that will be used to get the + // window level based on the metadata + hangingProtocolService.addCustomAttribute( + 'getPTVOIRange', + 'get PT VOI based on corrected or not', + props => { + const ptDisplaySet = props.find( + imageSet => imageSet.Modality === 'PT' + ); + + if (!ptDisplaySet) { + return; + } + + const { imageId } = ptDisplaySet.images[0]; + const imageIdScalingFactor = MetadataProvider.get( + 'scalingModule', + imageId + ); + + const isSUVAvailable = + imageIdScalingFactor && imageIdScalingFactor.suvbw; + + if (isSUVAvailable) { + return { + windowWidth: 5, + windowCenter: 2.5, + }; + } + + return; + } + ); }, onModeExit: ({ servicesManager }) => { const { diff --git a/platform/core/package.json b/platform/core/package.json index 8c80400b2..5b028125f 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -37,7 +37,7 @@ "@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2", "@cornerstonejs/codec-openjpeg": "^1.2.2", "@cornerstonejs/codec-openjph": "^2.4.2", - "dicom-parser": "^1.8.9", + "dicom-parser": "^1.8.21", "@ohif/ui": "^2.0.0" }, "dependencies": { diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index cfad9af3d..3b6c540c2 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -1,3 +1,5 @@ +import cloneDeep from 'lodash.clonedeep'; + import { PubSubService } from '../_shared/pubSubServiceInterface'; import sortBy from '../../utils/sortBy'; import ProtocolEngine from './ProtocolEngine'; @@ -53,6 +55,12 @@ export default class HangingProtocolService extends PubSubService { activeProtocolIds: string[]; // the current protocol that is being applied to the viewports in object format protocol: HangingProtocol.Protocol; + // The version of the protocol that must not be modified with customizations + // if it was defined in the protocol definition. This is a copy of the protocol + // that is used to recompute the computedOptions when necessary as we override + // the computedOptions in the protocol object itself. + _originalProtocol: HangingProtocol.Protocol; + stageIndex = 0; _commandsManager: CommandsManager; _servicesManager: ServicesManager; @@ -611,6 +619,12 @@ export default class HangingProtocolService extends PubSubService { return defaultReturn; } + const originalProtocol = this._originalProtocol; + let originalProtocolStage; + if (!(originalProtocol instanceof Function)) { + originalProtocolStage = originalProtocol.stages[this.stageIndex]; + } + // if we reach here, it means that the displaySetInstanceUIDs to be dropped // in the viewportIndex are valid, and we can proceed with the update. However // we need to check if the displaySets that the viewport were showing @@ -619,45 +633,44 @@ export default class HangingProtocolService extends PubSubService { // check if displaySetSelectors are used by other viewports, and // store the viewportIndex and displaySetInstanceUIDs that need to be updated - const viewportsToUpdate = []; protocolViewports.forEach((viewport, index) => { - let viewportNeedsUpdate; - for (const displaySet of viewport.displaySets) { - if ( + const viewportNeedsUpdate = viewport.displaySets.some( + displaySet => displaySet.id === displaySetSelectorId && (displaySet.matchedDisplaySetsIndex || 0) === matchedDisplaySetsIndex - ) { - viewportNeedsUpdate = true; - break; - } - } + ); if (viewportNeedsUpdate) { - // we can then loop over the displaySets and choose all of them, - // but for the one that matches the oldDisplaySetInstanceUID we need to - // replace it with the newDisplaySetInstanceUID - const { + // Try to recompute the viewport options based on the current + // viewportIndex that needs update but from its old/original un-computed + // viewport & displaySet options + if (originalProtocolStage) { + const originalViewport = originalProtocolStage.viewports[index]; + const originalViewportOptions = originalViewport.viewportOptions; + const originalDisplaySetOptions = originalViewport.displaySets; + + viewport.viewportOptions = this.getComputedOptions( + originalViewportOptions, + [newDisplaySetInstanceUID] + ); + + viewport.displaySets = this.getComputedOptions( + originalDisplaySetOptions, + [newDisplaySetInstanceUID] + ); + } + + const displaySetInstanceUIDs = []; + const displaySetOptions = []; + + this._updateDisplaySetInstanceUIDs( + viewport, + displaySetSelectorId, + newDisplaySetInstanceUID, + this.displaySetMatchDetails, displaySetInstanceUIDs, - displaySetOptions, - } = viewport.displaySets.reduce( - (acc, displaySet) => { - const { id } = displaySet; - - let { - displaySetInstanceUID: displaySetInstanceUIDToUse, - } = this.displaySetMatchDetails.get(id); - - if (displaySet.id === displaySetSelectorId) { - displaySetInstanceUIDToUse = newDisplaySetInstanceUID; - } - - acc.displaySetInstanceUIDs.push(displaySetInstanceUIDToUse); - acc.displaySetOptions.push(displaySet); - - return acc; - }, - { displaySetInstanceUIDs: [], displaySetOptions: [] } + displaySetOptions ); viewportsToUpdate.push({ @@ -672,6 +685,29 @@ export default class HangingProtocolService extends PubSubService { return viewportsToUpdate; } + private _updateDisplaySetInstanceUIDs( + viewport: HangingProtocol.Viewport, + displaySetSelectorId: string, + newDisplaySetInstanceUID: string, + displaySetMatchDetails: Map, + displaySetInstanceUIDs: string[], + displaySetOptions: HangingProtocol.DisplaySetOptions[] + ) { + viewport.displaySets.forEach(displaySet => { + const { id } = displaySet; + const { + displaySetInstanceUID: oldDisplaySetInstanceUID, + } = displaySetMatchDetails.get(id); + + displaySetInstanceUIDs.push( + displaySet.id === displaySetSelectorId + ? newDisplaySetInstanceUID + : oldDisplaySetInstanceUID + ); + displaySetOptions.push(displaySet); + }); + } + /** * Gets a computed options value, or a copy of the options * This allows computing values such as the initial image index to use @@ -681,28 +717,53 @@ export default class HangingProtocolService extends PubSubService { * simpler than recomputing the entire protocol. */ public getComputedOptions( - options: Record, + options: Record | Array>, displaySetUIDs: string[] - ) { - const computed = { ...options }; - let displaySets; - for (const key in computed) { - const value = computed[key]; - if (!value) continue; - if (value.custom) { - if (!displaySets) { - displaySets = this.displaySets.filter( - displaySet => - displaySetUIDs.indexOf(displaySet.displaySetInstanceUID) !== -1 - ); - } - computed[key] = this.customAttributeRetrievalCallbacks[ - value.custom - ].callback.call(computed, displaySets); - if (computed[key] === undefined) computed[key] = computed.defaultValue; + ): any { + // Base case: if options is an array, map over the array and recursively call getComputedOptions + if (Array.isArray(options)) { + return options.map(option => + this.getComputedOptions(option, displaySetUIDs) + ); + } + + if (options === null) return options; + if (typeof options !== 'object') return options; + + // If options is an object with a custom attribute, compute a new options object + if (options.custom) { + const displaySets = this.displaySets.filter(displaySet => + displaySetUIDs.includes(displaySet.displaySetInstanceUID) + ); + + const customKey = options.custom as string; + if (!(customKey in this.customAttributeRetrievalCallbacks)) { + throw new Error( + `Custom key "${customKey}" not found in customAttributeRetrievalCallbacks.` + ); + } + + const callback = this.customAttributeRetrievalCallbacks[customKey] + .callback; + let newOptions = callback.call(options, displaySets); + + if (newOptions === undefined) { + newOptions = options.defaultValue; + } + + return this.getComputedOptions(newOptions, displaySetUIDs); + } + + // If options is an object without a custom attribute, recursively call getComputedOptions on its properties + const newOptions = {} as Record; + for (const key in options) { + // if not undefined + if (options[key] !== undefined) { + newOptions[key] = this.getComputedOptions(options[key], displaySetUIDs); } } - return computed; + + return newOptions; } /** @@ -859,7 +920,8 @@ export default class HangingProtocolService extends PubSubService { try { if (!this.protocol || this.protocol.id !== protocol.id) { this.stageIndex = options?.stageIndex || 0; - this.protocol = this._copyProtocol(protocol); + this._originalProtocol = this._copyProtocol(protocol); + this.protocol = protocol; const { imageLoadStrategy } = protocol; if (imageLoadStrategy) { @@ -1522,6 +1584,6 @@ export default class HangingProtocolService extends PubSubService { } _copyProtocol(protocol: Protocol) { - return JSON.parse(JSON.stringify(protocol)); + return cloneDeep(protocol); } } diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md index 625215b04..67bbd7ec8 100644 --- a/platform/docs/docs/platform/extensions/modules/hpModule.md +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -423,7 +423,8 @@ As you can see in the hanging protocol we defined three viewports (but only show - `options` (optional): options for the display set - voi: windowing options for the display set (optional: windowWidth, windowCenter) - voiInverted: whether the VOI is inverted or not (optional) - - colormap: colormap for the display set (optional: 'hsv' etc.) + - colormap: colormap for the display set (optional, it is an object with `{ name }` and optional extra `opacityMapping` property) + - displayPreset: display preset for the display set (optional, used for 3D volume rendering. e.g., 'CT-Bone') ### Custom attribute diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md index 8343d9745..fb0c1e1d4 100644 --- a/platform/docs/docs/platform/services/data/HangingProtocolService.md +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -309,6 +309,8 @@ viewportOptions: { custom: 'sopInstanceLocation', // This is the value returned if the above doesn't return anything defaultValue: { index: 5 }, + } +} ``` ### Included Custom Attributes diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index e77a4abb7..67d328041 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -72,13 +72,20 @@ function ViewerViewportGrid(props) { } ); + const computedViewportOptions = hangingProtocolService.getComputedOptions( + viewportOptions, + displaySetUIDsToHang + ); + + const computedDisplaySetOptions = hangingProtocolService.getComputedOptions( + displaySetUIDsToHangOptions, + displaySetUIDsToHang + ); + return { displaySetInstanceUIDs: displaySetUIDsToHang, - displaySetOptions: displaySetUIDsToHangOptions, - viewportOptions: hangingProtocolService.getComputedOptions( - viewportOptions, - displaySetUIDsToHang - ), + displaySetOptions: computedDisplaySetOptions, + viewportOptions: computedViewportOptions, }; };