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:
1 parent
b7fff77e17
commit
803f638401
87 files changed
+3413
-1919
No files matched your search
+130
-68
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user