Skip to content
161 changes: 158 additions & 3 deletions extensions/cornerstone/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { updateSegmentBidirectionalStats } from './utils/updateSegmentationStats
import { generateSegmentationCSVReport } from './utils/generateSegmentationCSVReport';
import { getUpdatedViewportsForSegmentation } from './utils/hydrationUtils';
import { SegmentationRepresentations } from '@cornerstonejs/tools/enums';
import { DisplaySet } from 'platform/core/src/types';

const { DefaultHistoryMemo } = csUtils.HistoryMemo;
const toggleSyncFunctions = {
Expand Down Expand Up @@ -117,9 +118,13 @@ function commandsModule({
hangingProtocolService,
syncGroupService,
segmentationService,
userAuthenticationService,
displaySetService,
} = servicesManager.services as AppTypes.Services;

let _protocolViewportDataInitSubscription: { unsubscribe: () => void } | null = null;
let _protocolViewportDataChangedSubscription: { unsubscribe: () => void } | null = null;

function _getActiveViewportEnabledElement() {
return getActiveViewportEnabledElement(viewportGridService);
}
Expand All @@ -145,7 +150,99 @@ function commandsModule({
};
}

/**
* Retrieves derived segmentations (SEG/RTSTRUCT) that are not yet hydrated
* for the given display set UID.
*/
function getDerivedData(modalities: string[], displaySetUID: string): DisplaySet[] {
const currentDisplaySet = displaySetService.getDisplaySetByUID(displaySetUID);
if (!currentDisplaySet) {
return [];
}

const displaySetCache = displaySetService.getDisplaySetCache();
const allDisplaySets = Array.from(displaySetCache.values());

return allDisplaySets.filter((ds): ds is DisplaySet => {
const isValidModality = modalities.includes(ds.Modality);
if (!isValidModality) {
return false;
}

/** Check if this derived display set references the current display set */
const referencesDisplaySet =
ds.referencedDisplaySetInstanceUID === displaySetUID ||
ds.SeriesInstanceUID === currentDisplaySet.SeriesInstanceUID;

return referencesDisplaySet;
});
}

const loadDerivedDisplaySetsForActiveViewport = async (
modalities: string[],
onLoadComplete: (displaySet: any, activeViewportId: string) => Promise<void> | void
): Promise<boolean> => {
const activeViewportId = viewportGridService.getActiveViewportId();
if (!activeViewportId) {
console.warn('No active viewport found');
return false;
}

const displaySetInstanceUIDs =
viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
if (!displaySetInstanceUIDs?.length) {
console.warn('No display sets found for active viewport');
return false;
}

const primaryDisplaySetUID = displaySetInstanceUIDs[0];
const derivedDisplaySets = getDerivedData(modalities, primaryDisplaySetUID);
if (!derivedDisplaySets.length) {
console.warn('No derived data found for active viewport!');
return false;
}

const headers = userAuthenticationService.getAuthorizationHeader();

const loadPromises = derivedDisplaySets.map(async displaySet => {
try {
await displaySet.load({ headers });
await onLoadComplete(displaySet, activeViewportId);
} catch (error) {
console.error(`Failed to load segmentation ${displaySet.displaySetInstanceUID}:`, error);
}
});

await Promise.all(loadPromises);
return true;
};

const actions = {
loadSegmentationsForActiveViewport: async () => {
console.info('Loading segmentations for active viewport...');

const loaded = await loadDerivedDisplaySetsForActiveViewport(
['SEG', 'RTSTRUCT'],
async (displaySet, activeViewportId) => {
const representationType =
displaySet.Modality === 'SEG'
? Enums.SegmentationRepresentations.Labelmap
: Enums.SegmentationRepresentations.Contour;

segmentationService.addSegmentationRepresentation(activeViewportId, {
segmentationId: displaySet.displaySetInstanceUID,
type: representationType,
});
}
);

if (!loaded) {
console.warn('No derived segmentations found for active viewport');
return;
}

console.info('Segmentations loaded for active viewport.');
},
jumpToMeasurementViewport: ({ annotationUID, measurement }) => {
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
const { metadata } = measurement;
Expand Down Expand Up @@ -1306,16 +1403,62 @@ function commandsModule({
const command = protocol.callbacks.onViewportDataInitialized;
const numPanes = protocol.stages?.[stageIndex]?.viewports.length ?? 1;
let numPanesWithData = 0;
const { unsubscribe } = cornerstoneViewportService.subscribe(EVENT, evt => {

actions.detachProtocolViewportDataListener?.();

const subscription = cornerstoneViewportService.subscribe(EVENT, () => {
numPanesWithData++;

if (numPanesWithData === numPanes) {
commandsManager.run(...command);
commandsManager.run(command);

// Unsubscribe from the event
unsubscribe(EVENT);
subscription.unsubscribe();
_protocolViewportDataInitSubscription = null;
}
});

_protocolViewportDataInitSubscription = subscription;
},

detachProtocolViewportDataListener: () => {
if (_protocolViewportDataInitSubscription) {
_protocolViewportDataInitSubscription.unsubscribe();
_protocolViewportDataInitSubscription = null;
}
},

attachProtocolViewportDataChangedListener: ({ protocol, stageIndex }) => {
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
const command = protocol.callbacks.onViewportDataChanged;

actions.detachProtocolViewportDataChangedListener?.();

const subscription = cornerstoneViewportService.subscribe(
EVENT,
(evt: { viewportId?: string; viewportData?: unknown }) => {
const viewportId = evt?.viewportId;
if (!viewportId) {
return;
}

commandsManager.run(command, {
viewportId,
viewportData: evt.viewportData,
protocol,
stageIndex,
});
}
);

_protocolViewportDataChangedSubscription = subscription;
},

detachProtocolViewportDataChangedListener: () => {
if (_protocolViewportDataChangedSubscription) {
_protocolViewportDataChangedSubscription.unsubscribe();
_protocolViewportDataChangedSubscription = null;
}
},

setViewportPreset: ({ viewportId, preset }) => {
Expand Down Expand Up @@ -2211,6 +2354,9 @@ function commandsModule({
};

const definitions = {
loadSegmentationsForActiveViewport: {
commandFn: actions.loadSegmentationsForActiveViewport,
},
// The command here is to show the viewer context menu, as being the
// context menu
showCornerstoneContextMenu: {
Expand Down Expand Up @@ -2370,6 +2516,15 @@ function commandsModule({
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
detachProtocolViewportDataListener: {
commandFn: actions.detachProtocolViewportDataListener,
},
attachProtocolViewportDataChangedListener: {
commandFn: actions.attachProtocolViewportDataChangedListener,
},
detachProtocolViewportDataChangedListener: {
commandFn: actions.detachProtocolViewportDataChangedListener,
},
setViewportPreset: {
commandFn: actions.setViewportPreset,
},
Expand Down
3 changes: 3 additions & 0 deletions extensions/default/src/getHangingProtocolModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const defaultProtocol = {
modifiedDate: '2023-04-01',
availableTo: {},
editableBy: {},
callbacks: {
onViewportDataChanged: ['loadSegmentationsForActiveViewport'],
},
protocolMatchingRules: [],
toolGroupIds: ['default'],
// -1 would be used to indicate active only, whereas other values are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,10 @@ export default class HangingProtocolService extends PubSubService {
this.customAttributeRetrievalCallbacks
);

// Resets the full protocol status here.
this._commandsManager.run('detachProtocolViewportDataListener');
this._commandsManager.run('detachProtocolViewportDataChangedListener');

// Resets the full protocol status here
this.protocol = null;

if (protocolId && typeof protocolId === 'string') {
Expand Down Expand Up @@ -1032,6 +1035,10 @@ export default class HangingProtocolService extends PubSubService {

try {
if (!this.protocol || this.protocol.id !== protocol.id) {

this._commandsManager.run('detachProtocolViewportDataListener');
this._commandsManager.run('detachProtocolViewportDataChangedListener');

this.stageIndex = options?.stageIndex || 0;
//Reset load performed to false to re-fire loading strategy at new study opening
this.customImageLoadPerformed = false;
Expand Down Expand Up @@ -1212,8 +1219,18 @@ export default class HangingProtocolService extends PubSubService {

const { columns: numCols, rows: numRows, layoutOptions = [] } = layoutProps;

this._commandsManager.run('detachProtocolViewportDataListener');
this._commandsManager.run('detachProtocolViewportDataChangedListener');

if (this.protocol?.callbacks?.onViewportDataInitialized) {
this._commandsManager.runCommand('attachProtocolViewportDataListener', {
this._commandsManager.run('attachProtocolViewportDataListener', {
protocol: this.protocol,
stageIndex: this.stageIndex,
});
}

if (this.protocol?.callbacks?.onViewportDataChanged) {
this._commandsManager.run('attachProtocolViewportDataChangedListener', {
protocol: this.protocol,
stageIndex: this.stageIndex,
});
Expand Down
7 changes: 7 additions & 0 deletions platform/core/src/types/DisplaySet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export type DisplaySet = {

isHydrated?: boolean;
isRehydratable?: boolean;

/**
* Loads the display set.
* @param headers - The headers to use for the request.
* @returns A promise that resolves when the display set is loaded.
*/
load: ({ headers }: { headers?: unknown }) => Promise<void>;
};

export type DisplaySetSeriesMetadataInvalidatedEvent = {
Expand Down
9 changes: 9 additions & 0 deletions platform/core/src/types/HangingProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,15 @@ export type ProtocolNotifications = {
// and all viewport data includes a designated display set. This command
// will run on every stage's initial layout.
onViewportDataInitialized?: Command[];
// This set of commands is executed whenever viewport data has changed for a
// given viewport.
//
// Commands will receive an options object containing:
// - viewportId
// - viewportData
// - protocol
// - stageIndex
onViewportDataChanged?: Command[];
// This set of commands is executed before the stage change is applied
onStageChange?: Command[];
};
Expand Down
15 changes: 15 additions & 0 deletions platform/docs/docs/platform/extensions/modules/hpModule.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,17 @@ The `onLayoutChange` callback is executed before the layout change is started. Y

The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start.

### `onViewportDataChanged`

The `onViewportDataChanged` callback is executed whenever a viewport's data changes (for example, when a different display set is assigned to a viewport). This is useful for reacting to runtime changes like drag/drop swaps, layout tools that reassign data, etc.

Commands in `onViewportDataChanged` will receive an options object including:

- `viewportId`
- `viewportData`
- `protocol`
- `stageIndex`

Here is an example of how you can add these callbacks to your hanging protocol configuration:

```javascript
Expand All @@ -620,7 +631,11 @@ const protocol = {
onViewportDataInitialized: [
// Array of commands or actions to execute on viewport data initialization
],
onViewportDataChanged: [
// Array of commands or actions to execute when viewport data changes
],
},
// protocolMatchingRules
// the rest
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,4 @@ const protocol = {
// protocolMatchingRules
// the rest
};
```
Loading