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:
1 parent
045e54aa02
commit
1881ce5cbc
19 files changed
+240
-851
No files matched your search
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user