feat: state sync service and hanging protocol updates to preserve state (#3131)

* feat: Add state sync and use it to remember viewport grid info

fix: Version updates

Fixes for toggling MPR mode

Fix the display when the interleaved load module fails

Fix the memory of the state to restore correctly

PR fixes for the state sync service

PR fixes

PR fixes

PR fixes

Added a hack warning to remove volumeDeactivate

Fixes for TMTV colormap setting

Fix the casing

Missed renames

fix: tests not running due to variance in ordering

Reverting some fixes to change case

PR changes - mostly comments and minor improvements

fix: All display sets were being updated on drag and drop

PR fixes - mostly renames

PR fixes

Test support for OHIF, for HP branch

test: Add at least a minimal set of automated tests for hanging protocols

Docs

PR fixes

Merge fixes

DOCS updates

Add an example of the mn hanging protocol

PR fixes

PR fixes

PR fixes

* Fix the drag and drop

PR fixes

* PR changes - update default keys for next/previous stage

* fix: Was storing the custom viewport grid too aggressively

Caused by a PR change misspelling a variable
This commit is contained in:
Bill Wallace authored and GitHub committed 2023-03-15 12:41:41 -04:00
1 parent b7fff77e17
commit 803f638401
87 files changed
+3413 -1919

No files matched your search

@@ -1,4 +1,4 @@
import { pubSubServiceInterface } from '@ohif/core';
import { PubSubService } from '@ohif/core';
import {
RenderingEngine,
StackViewport,
@@ -21,6 +21,11 @@ import {
StackViewportData,
VolumeViewportData,
} from '../../types/CornerstoneCacheService';
import {
Presentation,
StackPresentation,
VolumePresentation,
} from '../../types/Presentation';
import {
setColormap,
setLowerUpperColorTransferFunction,
@@ -37,18 +42,14 @@ const EVENTS = {
* Handles cornerstone viewport logic including enabling, disabling, and
* updating the viewport.
*/
class CornerstoneViewportService implements IViewportService {
class CornerstoneViewportService extends PubSubService
implements IViewportService {
renderingEngine: Types.IRenderingEngine | null;
viewportsInfo: Map<number, ViewportInfo>;
viewportsInfo: Map<number, ViewportInfo> = new Map();
viewportsById: Map<string, ViewportInfo> = new Map();
viewportGridResizeObserver: ResizeObserver | null;
viewportsDisplaySets: Map<string, string[]> = new Map();
/**
* Service-specific
*/
EVENTS: { [key: string]: string };
listeners: { [key: string]: Array<(...args: any[]) => void> };
_broadcastEvent: unknown; // we should be able to extend the PubSub class to get this
// Some configs
enableResizeDetector: true;
resizeRefreshRateMs: 200;
@@ -56,15 +57,10 @@ class CornerstoneViewportService implements IViewportService {
servicesManager = null;
constructor(servicesManager) {
super(EVENTS);
this.renderingEngine = null;
this.viewportGridResizeObserver = null;
this.viewportsInfo = new Map();
//
this.listeners = {};
this.EVENTS = EVENTS;
this.servicesManager = servicesManager;
Object.assign(this, pubSubServiceInterface);
//
}
/**
@@ -77,12 +73,23 @@ class CornerstoneViewportService implements IViewportService {
viewportOptions: PublicViewportOptions,
elementRef: HTMLDivElement
) {
const viewportInfo = new ViewportInfo(
viewportIndex,
this.getViewportId(viewportIndex)
);
// Use the provided viewportId
// Not providing a viewportId is frowned upon because it does weird things
// on moving them around, but it does mostly work.
if (!viewportOptions.viewportId) {
console.warn('Should provide viewport id externally', viewportOptions);
viewportOptions.viewportId =
this.getViewportId(viewportIndex) || `viewport-${viewportIndex}`;
}
const { viewportId } = viewportOptions;
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
if (!viewportInfo.viewportId) {
throw new Error('Should have viewport ID afterwards');
}
viewportInfo.setElement(elementRef);
this.viewportsInfo.set(viewportIndex, viewportInfo);
this.viewportsById.set(viewportId, viewportInfo);
}
public getViewportIds(): string[] {
@@ -96,7 +103,7 @@ class CornerstoneViewportService implements IViewportService {
}
public getViewportId(viewportIndex: number): string {
return `viewport-${viewportIndex}`;
return this.viewportsInfo[viewportIndex]?.viewportId;
}
/**
@@ -163,6 +170,32 @@ class CornerstoneViewportService implements IViewportService {
this.viewportsInfo.get(viewportIndex).destroy();
this.viewportsInfo.delete(viewportIndex);
this.viewportsById.delete(viewportId);
}
public getPresentation(viewportIndex: number): Presentation {
const viewportInfo = this.viewportsInfo.get(viewportIndex);
if (!viewportInfo) return;
const {
presentationId: id,
viewportType,
} = viewportInfo.getViewportOptions();
if (!id) return;
const csViewport = this.getCornerstoneViewportByIndex(viewportIndex);
if (!csViewport) return;
const properties = csViewport.getProperties();
const initialImageIndex = csViewport.getCurrentImageIdIndex();
const camera = csViewport.getCamera();
return {
id,
viewportType:
!viewportType || viewportType === 'stack' ? 'stack' : 'volume',
properties,
initialImageIndex,
camera,
};
}
/**
@@ -177,29 +210,27 @@ class CornerstoneViewportService implements IViewportService {
viewportIndex: number,
viewportData: StackViewportData | VolumeViewportData,
publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[]
publicDisplaySetOptions: DisplaySetOptions[],
presentation?: Presentation
): void {
const renderingEngine = this.getRenderingEngine();
const viewportInfo = this.viewportsInfo.get(viewportIndex);
if (!publicViewportOptions.viewportId) {
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
const viewportId =
publicViewportOptions.viewportId || this.getViewportId(viewportIndex);
if (!viewportId) {
throw new Error('Must define viewportId externally');
}
let viewportId = viewportInfo.getViewportId();
const viewportInfo = this.viewportsById.get(viewportId);
// if currently there is a viewport with the viewportId, but it is not the same
// as the one we are trying to set, we need to disable the old one
// and enable the new one, we could ideally change the name of the viewportId
// but the viewportId is an integral part in renderers map, tools svg cache
// etc. which would require a lot of refactoring, for now we will just disable
// the old one and enable the new one at the end of this function
let newViewportId = null;
if (publicViewportOptions?.viewportId !== viewportId) {
newViewportId = publicViewportOptions.viewportId;
viewportInfo.setViewportId(newViewportId);
if (!viewportInfo) {
throw new Error('Viewport info not defined');
}
renderingEngine.disableElement(viewportId);
// If the viewport has moved index, then record the new index
if (viewportInfo.viewportIndex !== viewportIndex) {
this.viewportsInfo.delete(viewportInfo.viewportIndex);
this.viewportsInfo.set(viewportIndex, viewportInfo);
viewportInfo.viewportIndex = viewportIndex;
}
viewportInfo.setRenderingEngineId(renderingEngine.id);
@@ -220,9 +251,9 @@ class CornerstoneViewportService implements IViewportService {
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
viewportData,
viewportIndex,
viewportId,
});
viewportId = viewportInfo.getViewportId();
const element = viewportInfo.getElement();
const type = viewportInfo.getViewportType();
const background = viewportInfo.getBackground();
@@ -245,7 +276,7 @@ class CornerstoneViewportService implements IViewportService {
renderingEngine.enableElement(viewportInput);
const viewport = renderingEngine.getViewport(viewportId);
this._setDisplaySets(viewport, viewportData, viewportInfo);
this._setDisplaySets(viewport, viewportData, viewportInfo, presentation);
}
public getCornerstoneViewport(
@@ -308,8 +339,9 @@ class CornerstoneViewportService implements IViewportService {
_setStackViewport(
viewport: Types.IStackViewport,
viewportData: StackViewportData,
viewportInfo: ViewportInfo
) {
viewportInfo: ViewportInfo,
presentation?: StackPresentation
): void {
const displaySetOptions = viewportInfo.getDisplaySetOptions();
const {
@@ -320,29 +352,40 @@ class CornerstoneViewportService implements IViewportService {
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
let initialImageIndexToUse = initialImageIndex;
let initialImageIndexToUse =
presentation?.initialImageIndex ?? initialImageIndex;
if (!initialImageIndexToUse) {
if (
initialImageIndexToUse === undefined ||
initialImageIndexToUse === null
) {
initialImageIndexToUse =
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
}
const { voi, voiInverted } = displaySetOptions[0];
const properties = {};
if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
voi.windowWidth,
voi.windowCenter
);
properties.voiRange = { lower, upper };
const properties = presentation?.properties || {};
if (!presentation?.properties) {
const { voi, voiInverted } = displaySetOptions[0];
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 (voiInverted !== undefined) {
properties.invert = voiInverted;
}
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
// There is a bug in CS3D that the setStack does not
// navigate to the desired image.
viewport.setStack(imageIds, 0).then(() => {
// The scroll, however, works fine in CS3D
viewport.scroll(initialImageIndexToUse);
viewport.setProperties(properties);
if (presentation?.camera) viewport.setCamera(presentation.camera);
});
}
@@ -397,7 +440,8 @@ class CornerstoneViewportService implements IViewportService {
async _setVolumeViewport(
viewport: Types.IVolumeViewport,
viewportData: VolumeViewportData,
viewportInfo: ViewportInfo
viewportInfo: ViewportInfo,
presentation: VolumePresentation
): Promise<void> {
// TODO: We need to overhaul the way data sources work so requests can be made
// async. I think we should follow the image loader pattern which is async and
@@ -423,6 +467,7 @@ class CornerstoneViewportService implements IViewportService {
displaySetInstanceUIDs.push(displaySetInstanceUID);
if (!volume) {
console.log('Volume display set not found');
continue;
}
@@ -453,10 +498,15 @@ class CornerstoneViewportService implements IViewportService {
!hangingProtocolService.customImageLoadPerformed
) {
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
return hangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
});
if (
hangingProtocolService.runImageLoadStrategy({
viewportId: viewport.id,
volumeInputArray,
})
) {
// Fallback to the default strategy if the custom one fails
return;
}
}
volumeToLoad.forEach(volume => {
@@ -464,10 +514,10 @@ class CornerstoneViewportService implements IViewportService {
});
// This returns the async continuation only
return this.setVolumesForViewport(viewport, volumeInputArray);
return this.setVolumesForViewport(viewport, volumeInputArray, presentation);
}
public async setVolumesForViewport(viewport, volumeInputArray) {
public async setVolumesForViewport(viewport, volumeInputArray, presentation) {
const {
displaySetService,
segmentationService,
@@ -475,6 +525,9 @@ class CornerstoneViewportService implements IViewportService {
} = this.servicesManager.services;
await viewport.setVolumes(volumeInputArray);
const { properties, camera } = presentation || {};
if (properties) viewport.setProperties(properties);
if (camera) viewport.setCamera(camera);
// load any secondary displaySets
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
@@ -589,7 +642,11 @@ class CornerstoneViewportService implements IViewportService {
// Todo: keepCamera is an interim solution until we have a better solution for
// keeping the camera position when the viewport data is changed
public updateViewport(viewportIndex, viewportData, keepCamera = false) {
public updateViewport(
viewportIndex: number,
viewportData,
keepCamera = false
) {
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
const viewportId = viewportInfo.getViewportId();
@@ -645,19 +702,22 @@ class CornerstoneViewportService implements IViewportService {
_setDisplaySets(
viewport: StackViewport | VolumeViewport,
viewportData: StackViewportData | VolumeViewportData,
viewportInfo: ViewportInfo
viewportInfo: ViewportInfo,
presentation?: Presentation
): void {
if (viewport instanceof StackViewport) {
this._setStackViewport(
viewport,
viewportData as StackViewportData,
viewportInfo
viewportInfo,
presentation as StackPresentation
);
} else if (viewport instanceof VolumeViewport) {
this._setVolumeViewport(
viewport,
viewportData as VolumeViewportData,
viewportInfo
viewportInfo,
presentation as VolumePresentation
);
} else {
throw new Error('Unknown viewport type');
@@ -758,7 +818,7 @@ class CornerstoneViewportService implements IViewportService {
}
}
export default function ExtendedCornerstoneViewportService(serviceManager) {
export default function CornerstoneViewportServiceRegistration(serviceManager) {
return {
name: 'cornerstoneViewportService',
altName: 'CornerstoneViewportService',
@@ -767,3 +827,5 @@ export default function ExtendedCornerstoneViewportService(serviceManager) {
},
};
}
export { CornerstoneViewportService, CornerstoneViewportServiceRegistration };
@@ -18,6 +18,8 @@ export type ViewportOptions = {
viewportType: Enums.ViewportType;
toolGroupId: string;
viewportId: string;
// Presentation ID to store/load presentation state from
presentationId?: string;
orientation?: Types.Orientation;
background?: Types.Point3;
syncGroups?: SyncGroup[];
@@ -33,6 +35,7 @@ export type ViewportOptions = {
export type PublicViewportOptions = {
viewportType?: string;
toolGroupId?: string;
presentationId?: string;
viewportId?: string;
orientation?: string;
background?: Types.Point3;
@@ -42,6 +45,11 @@ export type PublicViewportOptions = {
allowUnmatchedView?: boolean;
};
export type DisplaySetSelector = {
id?: string;
options?: PublicDisplaySetOptions;
};
export type PublicDisplaySetOptions = {
voi?: VOI;
voiInverted?: boolean;
@@ -136,7 +144,7 @@ class ViewportInfo {
}
public setPublicDisplaySetOptions(
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
publicDisplaySetOptions: PublicDisplaySetOptions[] | DisplaySetSelector[]
): void {
// map the displaySetOptions and check if they are undefined then set them to default values
const displaySetOptions = this.mapDisplaySetOptions(
@@ -167,7 +175,10 @@ class ViewportInfo {
viewportOptionsEntry: PublicViewportOptions
): void {
let viewportType = viewportOptionsEntry.viewportType;
let toolGroupId = viewportOptionsEntry.toolGroupId;
const {
toolGroupId = DEFAULT_TOOLGROUP_ID,
presentationId,
} = viewportOptionsEntry;
let orientation;
if (!viewportType) {
@@ -185,16 +196,13 @@ class ViewportInfo {
orientation = Enums.OrientationAxis.AXIAL;
}
if (!toolGroupId) {
toolGroupId = DEFAULT_TOOLGROUP_ID;
}
this.setViewportOptions({
...viewportOptionsEntry,
viewportId: this.viewportId,
viewportType: viewportType as Enums.ViewportType,
orientation,
toolGroupId,
presentationId,
});
}
@@ -240,12 +248,15 @@ class ViewportInfo {
return this.viewportOptions.initialImageOptions;
}
// Handle incoming public display set options or a display set select
// with a contained options.
private mapDisplaySetOptions(
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
options: PublicDisplaySetOptions[] | DisplaySetSelector[] = [{}]
): Array<DisplaySetOptions> {
const displaySetOptions: Array<DisplaySetOptions> = [];
publicDisplaySetOptions.forEach(option => {
options.forEach(item => {
let option = item?.options || item;
if (!option) {
option = {
blendMode: undefined,