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 <wayfarer3130@gmail.com>
This commit is contained in:
AlirezaandBill Wallace authored and GitHub committed 2023-05-16 13:07:37 -04:00
1 parent 045e54aa02
commit 1881ce5cbc
19 files changed
+240 -851

No files matched your search

+1 -1
View File
@@ -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": {
@@ -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<string, HangingProtocol.DisplaySetMatchDetails>,
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<string, unknown>,
options: Record<string, unknown> | Array<Record<string, unknown>>,
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<string, unknown>;
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);
}
}
@@ -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
@@ -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
@@ -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,
};
};