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,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils } from '@ohif/core';
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import {
@@ -27,6 +27,7 @@ function OHIFCornerstoneSRViewport(props) {
dataSource,
displaySets,
viewportIndex,
viewportOptions,
viewportLabel,
servicesManager,
extensionManager,
@@ -215,6 +216,7 @@ function OHIFCornerstoneSRViewport(props) {
// override the activeImageDisplaySetData
displaySets={[activeImageDisplaySetData]}
viewportOptions={{
...viewportOptions,
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
}}
onElementEnabled={onElementEnabled}
@@ -419,6 +421,9 @@ OHIFCornerstoneSRViewport.propTypes = {
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
viewportOptions: PropTypes.object,
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
};
OHIFCornerstoneSRViewport.defaultProps = {
@@ -10,6 +10,7 @@ import {
utilities as csUtils,
CONSTANTS,
} from '@cornerstonejs/core';
import { Services } from '@ohif/core';
import { setEnabledElement } from '../state';
@@ -22,6 +23,9 @@ import {
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
import { CornerstoneViewportService } from '../services/ViewportService/CornerstoneViewportService';
import Presentation from '../types/Presentation';
const STACK = 'stack';
function areEqual(prevProps, nextProps) {
@@ -128,6 +132,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
cornerstoneViewportService,
cornerstoneCacheService,
viewportGridService,
stateSyncService,
} = servicesManager.services;
const cineHandler = () => {
@@ -211,6 +216,21 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
}, [elementRef]);
const storePresentation = () => {
const currentPresentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
const { presentationSync } = stateSyncService.getState();
if (currentPresentation) {
stateSyncService.store({
presentationSync: {
...presentationSync,
[currentPresentation.id]: currentPresentation,
},
});
}
};
const cleanUpServices = useCallback(() => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
@@ -288,6 +308,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
setImageScrollBarHeight();
return () => {
storePresentation();
cleanUpServices();
cornerstoneViewportService.disableElement(viewportIndex);
@@ -360,11 +382,20 @@ const OHIFCornerstoneViewport = React.memo(props => {
initialImageIndex
);
storePresentation();
const { presentationSync } = stateSyncService.getState();
const { presentationId } = viewportOptions;
const presentation = presentationId
? (presentationSync[presentationId] as Presentation)
: null;
cornerstoneViewportService.setViewportData(
viewportIndex,
viewportData,
viewportOptions,
displaySetOptions
displaySetOptions,
presentation
);
};
@@ -118,6 +118,10 @@ function ViewportOrientationMarkers({
viewportIndex
);
if (!ohifViewport) {
console.log('ViewportOrientationMarkers::No viewport');
return null;
}
const backgroundColor = ohifViewport.getViewportOptions().background;
// Todo: probably this can be done in a better way in which we identify bright
+20 -25
View File
@@ -15,11 +15,14 @@ import { ServicesManager } from '@ohif/core';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
import { setColormap } from './utils/colormap/transferFunctionHelpers';
import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol';
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
const commandsModule = ({ servicesManager }) => {
const commandsModule = ({
servicesManager,
}: {
servicesManager: ServicesManager;
}): React.FunctionComponent => {
const {
viewportGridService,
toolGroupService,
@@ -27,9 +30,8 @@ const commandsModule = ({ servicesManager }) => {
toolbarService,
uiDialogService,
cornerstoneViewportService,
hangingProtocolService,
uiNotificationService,
} = (servicesManager as ServicesManager).services;
} = servicesManager.services;
function _getActiveViewportEnabledElement() {
return getActiveViewportEnabledElement(viewportGridService);
@@ -128,6 +130,14 @@ const commandsModule = ({ servicesManager }) => {
});
viewport.render();
},
// Just call the toolbar service record interaction - allows
// executing a toolbar command as a full toolbar command with side affects
// coming from the ToolbarService itself.
toolbarServiceRecordInteraction: props => {
toolbarService.recordInteraction(props);
},
setToolActive: ({ toolName, toolGroupId = null }) => {
if (toolName === 'Crosshairs') {
const activeViewportToolGroup = _getToolGroup(null);
@@ -150,7 +160,7 @@ const commandsModule = ({ servicesManager }) => {
};
const toolGroup = _getToolGroup(toolGroupId);
const toolGroupViewportIds = toolGroup.getViewportIds();
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
// if toolGroup has been destroyed, or its viewports have been removed
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
@@ -404,16 +414,6 @@ const commandsModule = ({ servicesManager }) => {
(activeViewportIndex - 1 + viewports.length) % viewports.length;
viewportGridService.setActiveViewportIndex(nextViewportIndex);
},
setHangingProtocol: ({ protocolId }) => {
hangingProtocolService.setProtocol(protocolId);
},
toggleMPR: ({ toggledState }) => {
toggleMPRHangingProtocol({
toggledState,
servicesManager,
getToolGroup: _getToolGroup,
});
},
toggleStackImageSync: ({ toggledState }) => {
toggleStackImageSync({
getEnabledElement,
@@ -451,6 +451,11 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
toolbarServiceRecordInteraction: {
commandFn: actions.toolbarServiceRecordInteraction,
storeContexts: [],
options: {},
},
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
@@ -554,16 +559,6 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
setHangingProtocol: {
commandFn: actions.setHangingProtocol,
storeContexts: [],
options: {},
},
toggleMPR: {
commandFn: actions.toggleMPR,
storeContexts: [],
options: {},
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
storeContexts: [],
@@ -1,16 +1,46 @@
const mpr = {
id: 'mpr',
import { Types } from '@ohif/core';
const mpr: Types.HangingProtocol.Protocol = {
locked: true,
hasUpdatedPriorsInformation: false,
name: 'mpr',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2022-10-04T19:22:08.894Z',
modifiedDate: '2023-02-17',
availableTo: {},
editableBy: {},
// Unknown number of priors referenced - so just match any study
numberOfPriorsReferenced: 0,
protocolMatchingRules: [],
imageLoadStrategy: 'nth',
// imageLoadStrategy: 'nth',
callbacks: {
// Switches out of MPR mode when the layout change button is used
onLayoutChange: [
{
commandName: 'toggleHangingProtocol',
commandOptions: { protocolId: 'mpr' },
context: 'DEFAULT',
},
],
// Turns off crosshairs when switching out of MPR mode
onProtocolExit: [
{
commandName: 'toolbarServiceRecordInteraction',
commandOptions: {
interactionType: 'tool',
commands: [
{
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
},
},
],
},
displaySetSelectors: {
mprDisplaySet: {
activeDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
@@ -27,8 +57,7 @@ const mpr = {
},
stages: [
{
id: 'mpr3Stage',
name: 'mpr',
name: 'MPR 1x3',
viewportStructure: {
layoutType: 'grid',
properties: {
@@ -76,7 +105,7 @@ const mpr = {
},
displaySets: [
{
id: 'mprDisplaySet',
id: 'activeDisplaySet',
},
],
},
@@ -99,7 +128,7 @@ const mpr = {
},
displaySets: [
{
id: 'mprDisplaySet',
id: 'activeDisplaySet',
},
],
},
@@ -122,7 +151,7 @@ const mpr = {
},
displaySets: [
{
id: 'mprDisplaySet',
id: 'activeDisplaySet',
},
],
},
+4 -3
View File
@@ -26,6 +26,7 @@ import { registerColormap } from './utils/colormap/transferFunctionHelpers';
import { id } from './id';
import * as csWADOImageLoader from './initWADOImageLoader.js';
import { measurementMappingUtils } from './utils/measurementServiceMappings';
import { PublicViewportOptions } from './services/ViewportService/Viewport';
const Component = React.lazy(() => {
return import(
@@ -94,12 +95,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
// const onNewImageHandler = jumpData => {
// commandsManager.runCommand('jumpToImage', jumpData);
// };
const { ToolbarService } = servicesManager.services;
const { toolbarService } = (servicesManager as ServicesManager).services;
return (
<OHIFCornerstoneViewport
{...props}
ToolbarService={ToolbarService}
ToolbarService={toolbarService}
servicesManager={servicesManager}
commandsManager={commandsManager}
/>
@@ -151,4 +152,4 @@ const cornerstoneExtension: Types.Extensions.Extension = {
};
export default cornerstoneExtension;
export { measurementMappingUtils };
export { measurementMappingUtils, PublicViewportOptions };
+10 -5
View File
@@ -74,6 +74,7 @@ export default async function init({
hangingProtocolService,
toolGroupService,
viewportGridService,
stateSyncService,
} = servicesManager.services;
window.services = servicesManager.services;
@@ -97,6 +98,10 @@ export default async function init({
_showCPURenderingModal(uiModalService, hangingProtocolService);
}
// Stores a map from `presentationId` to a Presentation object so that
// an OHIFCornerstoneViewport can be redisplayed with the same attributes
stateSyncService.register('presentationSync', { clearOnModeExit: true });
const labelmapRepresentation =
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
@@ -369,8 +374,8 @@ export default async function init({
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
({ viewportIndex }) => {
const viewportId = `viewport-${viewportIndex}`;
({ viewportIndex, viewportId }) => {
viewportId = viewportId || `viewport-${viewportIndex}`;
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
@@ -427,9 +432,9 @@ function _showCPURenderingModal(uiModalService, hangingProtocolService) {
};
const { unsubscribe } = hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
({ progress }) => {
const done = callback(progress);
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
() => {
const done = callback(100);
if (done) {
unsubscribe();
@@ -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,
@@ -0,0 +1,23 @@
/** Store presentation data for either stack viewports or volume viewports */
import { Types } from '@cornerstonejs/core';
export interface BasePresentation {
id: string;
properties: Record<string, unknown>;
initialImageIndex?: number;
camera: Types.ICamera;
}
export interface StackPresentation extends BasePresentation {
viewportType: 'stack';
}
export interface VolumePresentation extends BasePresentation {
viewportType: 'volume';
}
// Currently it seems like the entire presentation state can be shared between
// Stack and Volume, but is setup to allow differences
export type Presentation = StackPresentation | VolumePresentation;
export default Presentation;
@@ -2,15 +2,17 @@ import { Enums } from '@cornerstonejs/core';
const STACK = 'stack';
const VOLUME = 'volume';
const ORTHOGRAPHIC = 'orthographic';
export default function getCornerstoneViewportType(
viewportType: string
): Enums.ViewportType {
if (viewportType.toLowerCase() === STACK) {
const lowerViewportType = viewportType.toLowerCase();
if (lowerViewportType === STACK) {
return Enums.ViewportType.STACK;
}
if (viewportType.toLowerCase() === VOLUME) {
if (lowerViewportType === VOLUME || lowerViewportType === ORTHOGRAPHIC) {
return Enums.ViewportType.ORTHOGRAPHIC;
}
@@ -1,301 +0,0 @@
import { Enums } from '@cornerstonejs/tools';
import removeToolGroupSegmentationRepresentations from '../removeToolGroupSegmentationRepresentations';
const MPR_TOOLGROUP_ID = 'mpr';
const cachedState = {
protocol: null,
stage: null,
viewportMatchDetails: null,
viewportStructure: null,
toolOptions: null,
};
const setCachedState = (
protocol,
stage,
viewportMatchDetails,
viewportStructure,
toolOptions
) => {
cachedState.protocol = protocol;
cachedState.stage = stage;
cachedState.viewportMatchDetails = viewportMatchDetails;
cachedState.viewportStructure = viewportStructure;
cachedState.toolOptions = JSON.parse(JSON.stringify(toolOptions));
};
const resetCachedState = () => {
cachedState.protocol = null;
cachedState.stage = null;
cachedState.viewportMatchDetails = null;
cachedState.viewportStructure = null;
cachedState.toolOptions = null;
};
export default function toggleMPRHangingProtocol({
toggledState,
servicesManager,
getToolGroup,
}) {
const {
uiNotificationService,
hangingProtocolService,
viewportGridService,
toolbarService,
} = servicesManager.services;
// TODO Introduce a service to persist the state of the current hanging protocol/app.
// So all of the code to persist the state here will no longer be needed. Perhaps
// just the id of the current hanging protocol to toggle MPR off is needed.
const {
activeViewportIndex,
viewports,
numRows,
numCols,
} = viewportGridService.getState();
const viewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex].displaySetInstanceUIDs;
// What is the current active protocol and stage number to restore later
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
const restoreErrorCallback = error => {
console.error(error);
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Something went wrong while trying to restore the previous layout.',
type: 'info',
duration: 3000,
});
};
if (toggledState) {
resetCachedState();
const {
viewportMatchDetails,
viewportStructure,
toolOptions,
} = _getViewportsInfo({
protocol,
stage,
viewports,
servicesManager,
});
setCachedState(
protocol,
stage,
viewportMatchDetails,
viewportStructure,
toolOptions
);
const matchDetails = {
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
};
_disableCrosshairs(
toolOptions.map(({ toolGroupId }) => toolGroupId),
getToolGroup
);
const errorCallback = error => {
// Unable to create MPR, so be sure to return to the cached/original protocol.
hangingProtocolService.setProtocol(
cachedState.protocol.id,
viewportMatchDetails,
restoreErrorCallback
);
uiNotificationService.show({
title: 'Multiplanar reconstruction (MPR) ',
message:
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
type: 'info',
duration: 3000,
});
};
hangingProtocolService.setProtocol(
MPR_TOOLGROUP_ID,
matchDetails,
errorCallback
);
return;
}
_disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup);
const { layoutType, properties } = cachedState.viewportStructure;
const { viewportMatchDetails } = cachedState;
// The reason we split the flow here is that we don't allow viewport grid
// change in the non default hanging protocol, so we can just apply the
// cached protocol and stage. However, for the default protocol, we need
// to also apply the layout type and properties.
if (cachedState.protocol.id !== 'default') {
hangingProtocolService.setProtocol(
cachedState.protocol.id,
viewportMatchDetails,
restoreErrorCallback
);
return;
}
hangingProtocolService.setProtocol(
'default',
viewportMatchDetails,
restoreErrorCallback
);
if (numRows !== properties.rows || numCols !== properties.columns) {
viewportGridService.setLayout({
numRows: properties.rows,
numCols: properties.columns,
layoutType,
layoutOptions: properties.layoutOptions,
});
}
const numViewports =
properties.layoutOptions.length || properties.rows * properties.columns;
// loop inside viewportMatchDetails map
// and set the viewportOptions for each viewport
[...Array(numViewports).keys()].forEach(viewportIndex => {
const viewportMatchDetailsForViewport = viewportMatchDetails.get(
viewportIndex
);
if (viewportMatchDetailsForViewport) {
const {
viewportOptions,
displaySetsInfo,
} = viewportMatchDetailsForViewport;
viewportGridService.setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs: displaySetsInfo.map(
displaySetInfo => displaySetInfo.displaySetInstanceUID
),
viewportOptions,
});
} else {
viewportGridService.setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs: [],
viewportOptions: {},
});
}
});
toolbarService.recordInteraction({
groupId: 'WindowLevel',
itemId: 'WindowLevel',
interactionType: 'tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'WindowLevel',
},
context: 'CORNERSTONE',
},
],
});
//clear segmentations if they exist
removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID);
}
function _disableCrosshairs(toolGroupIds, getToolGroup) {
toolGroupIds.forEach(toolGroupId => {
const toolGroup = getToolGroup(toolGroupId);
if (
toolGroup.getToolInstance('Crosshairs')?.mode === Enums.ToolModes.Active
) {
toolGroup.setToolDisabled('Crosshairs');
}
});
}
function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
// here we need to use the viewports and try to map it into the
// viewportMatchDetails and displaySetMatch that hangingProtocolService
// expects
const {
viewportGridService,
hangingProtocolService,
toolGroupService,
} = servicesManager.services;
const { numRows, numCols } = viewportGridService.getState();
let viewportMatchDetails = new Map();
const viewportStructure = {
layoutType: 'grid',
properties: {
rows: numRows,
columns: numCols,
layoutOptions: [],
},
};
viewports.forEach((viewport, viewportIndex) => {
viewportStructure.properties.layoutOptions.push({
x: viewport.x,
y: viewport.y,
width: viewport.width,
height: viewport.height,
});
});
if (protocol.id === 'default') {
viewports.forEach((viewport, viewportIndex) => {
if (viewport.displaySetInstanceUIDs) {
viewportMatchDetails.set(viewportIndex, {
displaySetsInfo: viewport.displaySetInstanceUIDs.map(
displaySetInstanceUID => {
return { displaySetInstanceUID };
}
),
viewportOptions: viewport.viewportOptions,
});
}
});
} else {
({ viewportMatchDetails } = hangingProtocolService.getMatchDetails());
}
// get the toolGroup state for viewports
let toolOptions = [];
const viewportIds = viewports
.map(
viewport =>
viewport.displaySetInstanceUIDs &&
viewport.displaySetInstanceUIDs.length > 0 &&
viewport.viewportOptions?.viewportId
)
.filter(Boolean);
if (viewportIds.length) {
toolOptions = viewportIds
.map(viewportId => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
return toolGroup
? {
toolGroupId: toolGroup.id,
toolOptions: toolGroup.toolOptions,
}
: null;
})
.filter(Boolean);
}
return { viewportMatchDetails, viewportStructure, toolOptions };
}
@@ -303,6 +303,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
seriesDate: ds.SeriesDate,
seriesTime: ds.SeriesTime,
numInstances: ds.numImageFrames,
countIcon: ds.countIcon,
StudyInstanceUID: ds.StudyInstanceUID,
componentType,
imageSrc,
@@ -1,10 +1,6 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
LayoutSelector as OHIFLayoutSelector,
ToolbarButton,
useViewportGrid,
} from '@ohif/ui';
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
import { ServicesManager } from '@ohif/core';
@@ -16,8 +12,6 @@ function LayoutSelector({
...rest
}) {
const [isOpen, setIsOpen] = useState(false);
const [disableSelector, setDisableSelector] = useState(false);
const [viewportGridState, viewportGridService] = useViewportGrid();
const {
hangingProtocolService,
@@ -50,43 +44,19 @@ function LayoutSelector({
};
}, [isOpen]);
useEffect(() => {
/* Reset to default layout when component unmounts */
return () => {
viewportGridService.setLayout({ numCols: 1, numRows: 1 });
};
}, []);
const onInteractionHandler = () => setIsOpen(!isOpen);
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
const onSelectionHandler = ({ numRows, numCols }) => {
// TODO Introduce a service to persist the state of the current hanging protocol/app.
// TODO Here the layout change will amount to a change of hanging protocol as specified by the extension for this layout selector tool
// followed by the change of the grid itself.
if (hangingProtocolService.getActiveProtocol().protocol.id === 'mpr') {
toolbarService.recordInteraction({
groupId: 'MPR',
itemId: 'MPR',
interactionType: 'toggle',
commands: [
{
commandName: 'toggleMPR',
commandOptions: {},
context: 'CORNERSTONE',
},
],
});
}
// When a new layout is selected, keep any extra/offscreen viewports
// so that if any of those viewports were populated via the UI then they
// will be maintained in case those viewports are redisplayed later.
viewportGridService.setLayout({
numRows,
numCols,
keepExtraViewports: true,
const onSelectionHandler = props => {
toolbarService.recordInteraction({
interactionType: 'action',
commands: [
{
commandName: 'setViewportGridLayout',
commandOptions: { ...props },
context: 'DEFAULT',
},
],
});
};
@@ -107,7 +77,7 @@ function LayoutSelector({
/>
)
}
isActive={disableSelector ? false : isOpen}
isActive={isOpen}
type="toggle"
/>
);
+12 -8
View File
@@ -14,7 +14,12 @@ import {
LoadingIndicatorProgress,
} from '@ohif/ui';
import i18n from '@ohif/i18n';
import { hotkeys } from '@ohif/core';
import {
ServicesManager,
HangingProtocolService,
hotkeys,
CommandsManager,
} from '@ohif/core';
import { useAppConfig } from '@state';
import Toolbar from '../Toolbar/Toolbar';
@@ -33,7 +38,7 @@ function ViewerLayout({
rightPanels = [],
leftPanelDefaultClosed = false,
rightPanelDefaultClosed = false,
}) {
}): React.FunctionComponent {
const [appConfig] = useAppConfig();
const navigate = useNavigate();
const location = useLocation();
@@ -169,15 +174,13 @@ function ViewerLayout({
useEffect(() => {
const { unsubscribe } = hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
// Todo: right now to set the loading indicator to false, we need to wait for the
// hangingProtocolService to finish applying the viewport matching to each viewport,
// however, this might not be the only approach to set the loading indicator to false. we need to explore this further.
({ progress }) => {
if (progress === 100) {
setShowLoadingIndicator(false);
}
() => {
setShowLoadingIndicator(false);
}
);
@@ -265,7 +268,8 @@ ViewerLayout.propTypes = {
extensionManager: PropTypes.shape({
getModuleEntry: PropTypes.func.isRequired,
}).isRequired,
commandsManager: PropTypes.object,
commandsManager: PropTypes.instanceOf(CommandsManager),
servicesManager: PropTypes.instanceOf(ServicesManager),
// From modes
leftPanels: PropTypes.array,
rightPanels: PropTypes.array,
-97
View File
@@ -1,97 +0,0 @@
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import React from 'react';
const commandsModule = ({ servicesManager, commandsManager }) => {
const {
measurementService,
hangingProtocolService,
uiNotificationService,
viewportGridService,
displaySetService,
} = servicesManager.services;
const actions = {
displayNotification: ({ text, title, type }) => {
uiNotificationService.show({
title: title,
message: text,
type: type,
});
},
clearMeasurements: () => {
measurementService.clear();
},
nextStage: () => {
// next stage in hanging protocols
hangingProtocolService.nextProtocolStage();
},
previousStage: () => {
hangingProtocolService.previousProtocolStage();
},
openDICOMTagViewer() {
const { activeViewportIndex, viewports } = viewportGridService.getState();
const activeViewportSpecificData = viewports[activeViewportIndex];
const { displaySetInstanceUIDs } = activeViewportSpecificData;
const displaySets = displaySetService.activeDisplaySets;
const { uiModalService } = servicesManager.services;
const displaySetInstanceUID = displaySetInstanceUIDs[0];
uiModalService.show({
content: DicomTagBrowser,
contentProps: {
displaySets,
displaySetInstanceUID,
onClose: uiModalService.hide,
},
title: 'DICOM Tag Browser',
});
},
/**
* Toggle viewport overlay (the information panel shown on the four corners
* of the viewport)
* @see ViewportOverlay and CustomizableViewportOverlay components
*/
toggleOverlays: () => {
const overlays = document.getElementsByClassName('viewport-overlay');
for (let i = 0; i < overlays.length; i++) {
overlays.item(i).classList.toggle('hidden');
}
},
};
const definitions = {
clearMeasurements: {
commandFn: actions.clearMeasurements,
storeContexts: [],
options: {},
},
displayNotification: {
commandFn: actions.displayNotification,
storeContexts: [],
options: {},
},
nextStage: {
commandFn: actions.nextStage,
storeContexts: [],
options: {},
},
previousStage: {
commandFn: actions.previousStage,
storeContexts: [],
options: {},
},
openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer,
},
};
return {
actions,
definitions,
defaultContext: 'DEFAULT',
};
};
export default commandsModule;
+386
View File
@@ -0,0 +1,386 @@
import { DicomMetadataStore, ServicesManager } from '@ohif/core';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import reuseCachedLayouts from './utils/reuseCachedLayouts';
import findViewportsByPosition, {
findOrCreateViewport as layoutFindOrCreate,
} from './findViewportsByPosition';
export type HangingProtocolParams = {
protocolId?: string;
stageIndex?: number;
activeStudyUID?: string;
stageId?: string;
};
/**
* Determine if a command is a hanging protocol one.
* For now, just use the two hanging protocol commands that are in this
* commands module, but if others get added elsewhere this may need enhancing.
*/
const isHangingProtocolCommand = command =>
command &&
(command.commandName === 'setHangingProtocol' ||
command.commandName === 'toggleHangingProtocol');
const commandsModule = ({ servicesManager, commandsManager }) => {
const {
measurementService,
hangingProtocolService,
uiNotificationService,
viewportGridService,
displaySetService,
stateSyncService,
toolbarService,
} = (servicesManager as ServicesManager).services;
const actions = {
displayNotification: ({ text, title, type }) => {
uiNotificationService.show({
title: title,
message: text,
type: type,
});
},
clearMeasurements: () => {
measurementService.clear();
},
/**
* Toggles off all tools which contain a commandName of setHangingProtocol
* or toggleHangingProtocol, and which match/don't match the protocol id/stage
*/
toggleHpTools: () => {
const {
protocol,
stageIndex: toggleStageIndex,
stage,
} = hangingProtocolService.getActiveProtocol();
const enableListener = button => {
if (!button.id) return;
const { commands, items } = button.props || button;
if (items) {
items.forEach(enableListener);
}
const hpCommand = commands?.find?.(isHangingProtocolCommand);
if (!hpCommand) return;
const { protocolId, stageIndex, stageId } = hpCommand.commandOptions;
const isActive =
(!protocolId || protocolId === protocol.id) &&
(stageIndex === undefined || stageIndex === toggleStageIndex) &&
(!stageId || stageId === stage.id);
toolbarService.setActive(button.id, isActive);
};
Object.values(toolbarService.getButtons()).forEach(enableListener);
},
/**
* Sets the specified protocol
* 1. Records any existing state using the viewport grid service
* 2. Finds the destination state - this can be one of:
* a. The specified protocol stage
* b. An alternate (toggled or restored) protocol stage
* c. A restored custom layout
* 3. Finds the parameters for the specified state
* a. Gets the displaySetSelectorMap
* b. Gets the map by position
* c. Gets any toggle mapping to map position to/from current view
* 4. If restore, then sets layout
* a. Maps viewport position by currently displayed viewport map id
* b. Uses toggle information to map display set id
* 5. Else applies the hanging protocol
* a. HP Service is provided displaySetSelectorMap
* b. HP Service will throw an exception if it isn't applicable
* @param options - contains information on the HP to apply
* @param options.activeStudyUID - the updated study to apply the HP to
* @param options.protocolId - the protocol ID to change to
* @param options.stageId - the stageId to apply
* @param options.stageIndex - the index of the stage to go to.
*/
setHangingProtocol: ({
activeStudyUID = '',
protocolId,
stageId,
stageIndex,
}: HangingProtocolParams): boolean => {
try {
// Stores in the state the reuseID to displaySetUID mapping
// Pass in viewportId for the active viewport. This item will get set as
// the activeViewportId
const state = viewportGridService.getState();
const hpInfo = hangingProtocolService.getState();
const {
protocol: oldProtocol,
} = hangingProtocolService.getActiveProtocol();
const stateSyncReduce = reuseCachedLayouts(
state,
hangingProtocolService,
stateSyncService
);
const {
hangingProtocolStageIndexMap,
viewportGridStore,
displaySetSelectorMap,
} = stateSyncReduce;
if (!protocolId) {
// Re-use the previous protocol id, and optionally stage
protocolId = hpInfo.protocolId;
if (stageId === undefined && stageIndex === undefined) {
stageIndex = hpInfo.stageIndex;
}
} else if (stageIndex === undefined && stageId === undefined) {
// Re-set the same stage as was previously used
const hangingId = `${activeStudyUID ||
hpInfo.activeStudyUID}:${protocolId}`;
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
}
const useStageIdx =
stageIndex ??
hangingProtocolService.getStageIndex(protocolId, {
stageId,
stageIndex,
});
if (activeStudyUID) {
hangingProtocolService.setActiveStudyUID(activeStudyUID);
}
const storedHanging = `${hangingProtocolService.getState().activeStudyUID
}:${protocolId}:${useStageIdx || 0}`;
const restoreProtocol = !!viewportGridStore[storedHanging];
if (
protocolId === hpInfo.hangingProtocolId &&
useStageIdx === hpInfo.stageIdx &&
!activeStudyUID
) {
// Clear the HP setting to reset them
hangingProtocolService.setProtocol(protocolId, {
stageId,
stageIndex: useStageIdx,
});
} else {
hangingProtocolService.setProtocol(protocolId, {
displaySetSelectorMap,
stageId,
stageIndex: useStageIdx,
restoreProtocol,
});
if (restoreProtocol) {
viewportGridService.set(viewportGridStore[storedHanging]);
}
}
// Do this after successfully applying the update
stateSyncService.store(stateSyncReduce);
// This is a default action applied
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
// Send the notification about updating the state
if (protocolId !== hpInfo.protocolId) {
const { protocol } = hangingProtocolService.getActiveProtocol();
// The old protocol callbacks are used for turning off things
// like crosshairs when moving to the new HP
commandsManager.run(oldProtocol.callbacks?.onProtocolExit);
// The new protocol callback is used for things like
// activating modes etc.
commandsManager.run(protocol.callbacks?.onProtocolEnter);
}
return true;
} catch (e) {
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
uiNotificationService.show({
title: 'Apply Hanging Protocol',
message: `The hanging protocol could not be applied due to ${e}`,
type: 'error',
duration: 3000,
});
return false;
}
},
toggleHangingProtocol: ({
protocolId,
stageIndex,
}: HangingProtocolParams): boolean => {
const {
protocol,
stageIndex: desiredStageIndex,
activeStudy,
} = hangingProtocolService.getActiveProtocol();
const { toggleHangingProtocol } = stateSyncService.getState();
const storedHanging = `${activeStudy.StudyInstanceUID
}:${protocolId}:${stageIndex | 0}`;
if (
protocol.id === protocolId &&
(stageIndex === undefined || stageIndex === desiredStageIndex)
) {
// Toggling off - restore to previous state
const previousState = toggleHangingProtocol[storedHanging] || {
protocolId: 'default',
};
return actions.setHangingProtocol(previousState);
} else {
stateSyncService.store({
toggleHangingProtocol: {
...toggleHangingProtocol,
[storedHanging]: {
protocolId: protocol.id,
stageIndex: desiredStageIndex,
},
},
});
return actions.setHangingProtocol({ protocolId, stageIndex });
}
},
deltaStage: ({ direction }) => {
const {
protocolId,
stageIndex: oldStageIndex,
} = hangingProtocolService.getState();
const { protocol } = hangingProtocolService.getActiveProtocol();
for (
let stageIndex = oldStageIndex + direction;
stageIndex >= 0 && stageIndex < protocol.stages.length;
stageIndex += direction
) {
if (protocol.stages[stageIndex].status !== 'disabled') {
return actions.setHangingProtocol({
protocolId,
stageIndex,
});
}
}
uiNotificationService.show({
title: 'Change Stage',
message: 'The hanging protocol has no more applicable stages',
type: 'error',
duration: 3000,
});
},
/**
* Changes the viewport grid layout in terms of the MxN layout.
*/
setViewportGridLayout: ({ numRows, numCols }) => {
const { protocol } = hangingProtocolService.getActiveProtocol();
const onLayoutChange = protocol.callbacks?.onLayoutChange;
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
console.log(
'setViewportGridLayout running',
onLayoutChange,
numRows,
numCols
);
// Don't apply the layout if the run command returns false
return;
}
const completeLayout = () => {
const state = viewportGridService.getState();
const stateReduce = findViewportsByPosition(
state,
{ numRows, numCols },
stateSyncService
);
const findOrCreateViewport = layoutFindOrCreate.bind(
null,
hangingProtocolService,
stateReduce.viewportsByPosition
);
viewportGridService.setLayout({
numRows,
numCols,
findOrCreateViewport,
});
stateSyncService.store(stateReduce);
};
// Need to finish any work in the callback
window.setTimeout(completeLayout, 0);
},
openDICOMTagViewer() {
const { activeViewportIndex, viewports } = viewportGridService.getState();
const activeViewportSpecificData = viewports[activeViewportIndex];
const { displaySetInstanceUIDs } = activeViewportSpecificData;
const displaySets = displaySetService.activeDisplaySets;
const { UIModalService } = servicesManager.services;
const displaySetInstanceUID = displaySetInstanceUIDs[0];
UIModalService.show({
content: DicomTagBrowser,
contentProps: {
displaySets,
displaySetInstanceUID,
onClose: UIModalService.hide,
},
title: 'DICOM Tag Browser',
});
},
/**
* Toggle viewport overlay (the information panel shown on the four corners
* of the viewport)
* @see ViewportOverlay and CustomizableViewportOverlay components
*/
toggleOverlays: () => {
const overlays = document.getElementsByClassName('viewport-overlay');
for (let i = 0; i < overlays.length; i++) {
overlays.item(i).classList.toggle('hidden');
}
},
};
const definitions = {
clearMeasurements: {
commandFn: actions.clearMeasurements,
storeContexts: [],
options: {},
},
displayNotification: {
commandFn: actions.displayNotification,
storeContexts: [],
options: {},
},
setHangingProtocol: {
commandFn: actions.setHangingProtocol,
storeContexts: [],
options: {},
},
toggleHangingProtocol: {
commandFn: actions.toggleHangingProtocol,
storeContexts: [],
options: {},
},
nextStage: {
commandFn: actions.deltaStage,
storeContexts: [],
options: { direction: 1 },
},
previousStage: {
commandFn: actions.deltaStage,
storeContexts: [],
options: { direction: -1 },
},
setViewportGridLayout: {
commandFn: actions.setViewportGridLayout,
storeContexts: [],
options: {},
},
openDICOMTagViewer: {
commandFn: actions.openDICOMTagViewer,
},
};
return {
actions,
definitions,
defaultContext: 'DEFAULT',
};
};
export default commandsModule;
@@ -0,0 +1,106 @@
import { StateSyncService, Types } from '@ohif/core';
/**
* This find or create viewport is paired with the reduce results from
* below, and the action of this viewport is to look for previously filled
* viewports, and to re-use by position id. If there is no filled viewport,
* then one can be re-used from the display set if it isn't going to be displayed.
* @param hangingProtocolService - bound parameter supplied before using this
* @param viewportsByPosition - bound parameter supplied before using this
* @param viewportIndex - the index to retrieve
* @param positionId - the current position on screen to retrieve
* @param options - the set of options used, so that subsequent calls can
* store state that is reset by the setLayout.
* This class uses the options to store the already viewed
* display sets, filling it initially with the pre-existing viewports.
*/
export const findOrCreateViewport = (
hangingProtocolService,
viewportsByPosition,
viewportIndex: number,
positionId: string,
options: Record<string, unknown>
) => {
const byPositionViewport = viewportsByPosition?.[positionId];
if (byPositionViewport) return { ...byPositionViewport };
const { protocolId, stageIndex } = hangingProtocolService.getState();
// Setup the initial in display correctly for initial view/select
if (!options.inDisplay) {
options.inDisplay = [...viewportsByPosition.initialInDisplay];
}
// See if there is a default viewport for new views.
const missing = hangingProtocolService.getMissingViewport(
protocolId,
stageIndex,
options
);
if (missing) {
const displaySetInstanceUIDs = missing.displaySetsInfo.map(
it => it.displaySetInstanceUID
);
options.inDisplay.push(...displaySetInstanceUIDs);
return {
displaySetInstanceUIDs,
displaySetOptions: missing.displaySetsInfo.map(
it => it.displaySetOptions
),
viewportOptions: {
...missing.viewportOptions,
},
};
}
return {};
};
/**
* Records the information on what viewports are displayed in which position.
* Also records what instances from the existing positions are going to be in
* view initially.
* @param state is the viewport grid state
* @param syncService is the state sync service to use for getting existing state
* @returns Set of states that can be applied to the state sync to remember
* the current view state.
*/
const findViewportsByPosition = (
state,
{ numRows, numCols },
syncService: StateSyncService
): Record<string, Record<string, unknown>> => {
const { viewports } = state;
const syncState = syncService.getState();
const viewportsByPosition = { ...syncState.viewportsByPosition };
const initialInDisplay = [];
for (const viewport of viewports) {
if (viewport.positionId) {
const storedViewport = {
...viewport,
viewportOptions: { ...viewport.viewportOptions },
};
viewportsByPosition[viewport.positionId] = storedViewport;
// The cache doesn't store the viewport options - it is only useful
// for remembering the type of viewport and UIDs
delete storedViewport.viewportId;
delete storedViewport.viewportOptions.viewportId;
}
}
for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) {
const pos = col + row * numCols;
const positionId = viewports?.[pos]?.positionId || `${col}-${row}`;
const viewport = viewportsByPosition[positionId];
if (viewport?.displaySetInstanceUIDs) {
initialInDisplay.push(...viewport.displaySetInstanceUIDs);
}
}
}
// Store the initially displayed elements
viewportsByPosition.initialInDisplay = initialInDisplay;
return { viewportsByPosition };
};
export default findViewportsByPosition;
@@ -1,6 +1,9 @@
const defaultProtocol = {
id: 'default',
locked: true,
// Don't store this hanging protocol as it applies to the currently active
// display set by default
// cacheId: null,
hasUpdatedPriorsInformation: false,
name: 'Default',
createdDate: '2021-02-23T19:22:08.894Z',
@@ -9,6 +12,25 @@ const defaultProtocol = {
editableBy: {},
protocolMatchingRules: [],
toolGroupIds: ['default'],
// -1 would be used to indicate active only, whereas other values are
// the number of required priors referenced - so 0 means active with
// 0 or more priors.
numberOfPriorsReferenced: 0,
// Default viewport is used to define the viewport when
// additional viewports are added using the layout tool
defaultViewport: {
viewportOptions: {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: -1,
},
],
},
displaySetSelectors: {
defaultDisplaySetId: {
// Unused currently
@@ -24,12 +46,12 @@ const defaultProtocol = {
},
},
],
studyMatchingRules: [],
// Can be used to select matching studies
// studyMatchingRules: [],
},
},
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
name: 'default',
viewportStructure: {
layoutType: 'grid',
@@ -38,6 +60,119 @@ const defaultProtocol = {
columns: 1,
},
},
viewports: [
{
viewportOptions: {
viewportType: 'stack',
toolGroupId: 'default',
// initialImageOptions: {
// index: 180,
// preset: 'middle', // 'first', 'last', 'middle'
// },
},
displaySets: [
{
id: 'defaultDisplaySetId',
},
],
},
],
createdDate: '2021-02-23T18:32:42.850Z',
},
// This is an example of a 2x2 layout that requires at least 2 viewports
// filled to be navigatable to
{
name: '2x2',
// Indicate that the number of viewports needed is 2 filled viewports,
// but that 4 viewports are preferred.
stageActivation: {
enabled: {
minViewportsMatched: 4,
},
passive: {
minViewportsMatched: 2,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 3,
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 2,
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 1,
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
// initialImageOptions: {
// index: 180,
// preset: 'middle', // 'first', 'last', 'middle'
// },
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: 0,
},
],
},
],
},
// This is an example of a layout with more than one element in it
// It can be navigated to using , and . (prev/next stage)
{
name: '1x2',
// Indicate that the number of viewports needed is 1 filled viewport,
// but that 2 viewports are preferred.
stageActivation: {
enabled: {
minViewportsMatched: 3,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
@@ -53,11 +188,26 @@ const defaultProtocol = {
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
// initialImageOptions: {
// index: 180,
// preset: 'middle', // 'first', 'last', 'middle'
// },
},
displaySets: [
{
id: 'defaultDisplaySetId',
// Shows the second index of this image set
matchedDisplaySetsIndex: 1,
},
],
},
],
createdDate: '2021-02-23T18:32:42.850Z',
},
],
numberOfPriorsReferenced: -1,
};
function getHangingProtocolModule() {
@@ -29,6 +29,7 @@ const makeDisplaySet = instances => {
SeriesDescription: instance.SeriesDescription || '',
Modality: instance.Modality,
isMultiFrame: isMultiFrame(instance),
countIcon: displayReconstructableInfo.value ? 'icon-mpr' : undefined,
numImageFrames: instances.length,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
isReconstructable: displayReconstructableInfo.value,
+29
View File
@@ -11,6 +11,7 @@ const metadataProvider = classes.MetadataProvider;
* @param {Object} configuration
*/
export default function init({ servicesManager, configuration }) {
const { stateSyncService } = servicesManager.services;
// Add
DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
@@ -23,6 +24,34 @@ export default function init({ servicesManager, configuration }) {
DicomMetadataStore.EVENTS.SERIES_UPDATED,
handlePETImageMetadata
);
// viewportGridStore is a sync state which stores the entire
// ViewportGridService getState, by the keys `<activeStudyUID>:<protocolId>:<stageIndex>`
// Used to recover manual changes to the layout of a stage.
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
// displaySetSelectorMap stores a map from
// `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to
// a displaySetInstanceUID, used to display named display sets in
// specific spots within a hanging protocol and be able to remember what the
// user did with those named spots between stages and protocols.
stateSyncService.register('displaySetSelectorMap', { clearOnModeExit: true });
// Stores a map from `<activeStudyUID>:${protocolId}` to the getHPInfo results
// in order to recover the correct stage when returning to a Hanging Protocol.
stateSyncService.register('hangingProtocolStageIndexMap', {
clearOnModeExit: true,
});
// Stores a map from the to be applied hanging protocols `<activeStudyUID>:<protocolId>`
// to the previously applied hanging protolStageIndexMap key, in order to toggle
// off the applied protocol and remember the old state.
stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
// Stores the viewports by `rows-cols` position so that when the layout
// changes numRows and numCols, the viewports can be remembers and then replaced
// afterwards.
stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
}
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
@@ -0,0 +1,75 @@
import { HangingProtocolService, StateSyncService, Types } from '@ohif/core';
export type ReturnType = {
hangingProtocolStageIndexMap: Record<string, Types.HangingProtocol.HPInfo>;
viewportGridStore: Record<string, unknown>;
displaySetSelectorMap: Record<string, string>;
};
/**
* Calculates a set of state information for hanging protocols and viewport grid
* which defines the currently applied hanging protocol state.
* @param state is the viewport grid state
* @param syncService is the state sync service to use for getting existing state
* @returns Set of states that can be applied to the state sync to remember
* the current view state.
*/
const reuseCachedLayout = (
state,
hangingProtocolService: HangingProtocolService,
syncService: StateSyncService
): ReturnType => {
const { activeViewportIndex, viewports, layout } = state;
const hpInfo = hangingProtocolService.getState();
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
const { protocol } = hangingProtocolService.getActiveProtocol();
const stage = protocol.stages[stageIndex];
const storeId = `${activeStudyUID}:${protocolId}:${stageIndex}`;
const syncState = syncService.getState();
const cacheId = `${activeStudyUID}:${protocolId}`;
const viewportGridStore = { ...syncState.viewportGridStore };
const hangingProtocolStageIndexMap = {
...syncState.hangingProtocolStageIndexMap,
};
const displaySetSelectorMap = { ...syncState.displaySetSelectorMap };
const { rows, columns } = stage.viewportStructure.properties;
const custom =
stage.viewports.length !== state.viewports.length ||
state.layout.numRows !== rows ||
state.layout.numCols !== columns;
hangingProtocolStageIndexMap[cacheId] = hpInfo;
if (storeId && custom) {
viewportGridStore[storeId] = { ...state };
}
for (let idx = 0; idx < state.viewports.length; idx++) {
const viewport = state.viewports[idx];
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
if (!displaySetOptions) continue;
for (let i = 0; i < displaySetOptions.length; i++) {
const displaySetUID = displaySetInstanceUIDs[i];
if (!displaySetUID) continue;
if (idx === activeViewportIndex && i === 0) {
displaySetSelectorMap[
`${activeStudyUID}:activeDisplaySet:0`
] = displaySetUID;
}
if (displaySetOptions[i]?.id) {
displaySetSelectorMap[
`${activeStudyUID}: ${displaySetOptions[i].id}: ${displaySetOptions[i]
.matchedDisplaySetsIndex || 0}`
] = displaySetUID;
}
}
}
return {
hangingProtocolStageIndexMap,
viewportGridStore,
displaySetSelectorMap,
};
};
export default reuseCachedLayout;
@@ -7,12 +7,12 @@ const RESPONSE = {
};
function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService } = servicesManager.services;
const { uiViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
return new Promise(async function(resolve, reject) {
let promptResult = await _askTrackMeasurements(
UIViewportDialogService,
uiViewportDialogService,
viewportIndex
);
@@ -25,7 +25,7 @@ function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
});
}
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
function _askTrackMeasurements(uiViewportDialogService, viewportIndex) {
return new Promise(function(resolve, reject) {
const message = 'Track measurements for this series?';
const actions = [
@@ -49,11 +49,11 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
},
];
const onSubmit = result => {
UIViewportDialogService.hide();
uiViewportDialogService.hide();
resolve(result);
};
UIViewportDialogService.show({
uiViewportDialogService.show({
viewportIndex,
id: 'measurement-tracking-prompt-begin-tracking',
type: 'info',
@@ -61,7 +61,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
actions,
onSubmit,
onOutsideClick: () => {
UIViewportDialogService.hide();
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
});
@@ -16,7 +16,7 @@ function promptHydrateStructuredReport(
evt
) {
const {
UIViewportDialogService,
uiViewportDialogService,
displaySetService,
} = servicesManager.services;
const { viewportIndex, displaySetInstanceUID } = evt;
@@ -26,7 +26,7 @@ function promptHydrateStructuredReport(
return new Promise(async function(resolve, reject) {
const promptResult = await _askTrackMeasurements(
UIViewportDialogService,
uiViewportDialogService,
viewportIndex
);
@@ -55,7 +55,7 @@ function promptHydrateStructuredReport(
});
}
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
function _askTrackMeasurements(uiViewportDialogService, viewportIndex) {
return new Promise(function(resolve, reject) {
const message =
'Do you want to continue tracking measurements for this study?';
@@ -72,18 +72,18 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
},
];
const onSubmit = result => {
UIViewportDialogService.hide();
uiViewportDialogService.hide();
resolve(result);
};
UIViewportDialogService.show({
uiViewportDialogService.show({
viewportIndex,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
UIViewportDialogService.hide();
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
});
@@ -33,7 +33,7 @@ function promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt) {
});
}
function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) {
function _askShouldAddMeasurements(uiViewportDialogService, viewportIndex) {
return new Promise(function(resolve, reject) {
const message =
'Do you want to add this measurement to the existing report?';
@@ -51,18 +51,18 @@ function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) {
},
];
const onSubmit = result => {
UIViewportDialogService.hide();
uiViewportDialogService.hide();
resolve(result);
};
UIViewportDialogService.show({
uiViewportDialogService.show({
viewportIndex,
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
UIViewportDialogService.hide();
uiViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
});
@@ -453,6 +453,7 @@ function _mapDisplaySets(
modality: ds.Modality,
seriesDate: formatDate(ds.SeriesDate),
numInstances: ds.numImageFrames,
countIcon: ds.countIcon,
StudyInstanceUID: ds.StudyInstanceUID,
componentType,
imageSrc,
@@ -0,0 +1 @@
export default (study, extraData) => Math.max(...(extraData?.displaySets?.map?.(ds => (ds.numImageFrames ?? 0))) || [0]);
@@ -0,0 +1 @@
export default (study, extraData) => extraData?.displaySets?.length;
@@ -0,0 +1,5 @@
export default (study, extraData) => {
const ret = extraData?.displaySets?.filter(ds => ds.numImageFrames>0)?.length;
console.log("number of display sets with images", ret);
return ret;
};
@@ -0,0 +1,33 @@
/**
* This function extracts an attribute from the already matched display sets, and
* compares it to the attribute in the current display set, and indicates if they match.
* From 'this', it uses:
* `sameAttribute` as the attribute name to look for
* `sameDisplaySetId` as the display set id to look for
* From `options`, it looks for
*/
export default function (displaySet, options) {
const { sameAttribute, sameDisplaySetId } = this;
if( !sameAttribute ) {
console.log("sameAttribute not defined in", this);
return `sameAttribute not defined in ${this.id}`;
}
if( !sameDisplaySetId ) {
console.log("sameDisplaySetId not defined in", this);
return `sameDisplaySetId not defined in ${this.id}`;
}
const { displaySetMatchDetails, displaySets } = options;
const match = displaySetMatchDetails.get(sameDisplaySetId);
if( !match ) {
console.log("No match for display set", sameDisplaySetId);
return false;
}
const { displaySetInstanceUID } = match;
const altDisplaySet = displaySets.find(it => it.displaySetInstanceUID==displaySetInstanceUID);
if( !altDisplaySet ) {
console.log("No display set found with", displaySetInstanceUID, "in", displaySets);
return false;
}
const testValue = altDisplaySet[sameAttribute];
return testValue===displaySet[sameAttribute];
}
@@ -0,0 +1 @@
export default (study, extraData) => extraData?.displaySets?.map(ds => ds.SeriesDescription);
+259
View File
@@ -0,0 +1,259 @@
import { Types } from '@ohif/core';
/**
* This hanging protocol has multiple stages, which are enabled when
* there are enough display sets with images to fill the stage, and
* are passive when there is at least one display set.
* Enabled display sets are navigated to by default, while passive ones
* are navigated to manually using the ctrl+end keyboard shortcut.
*/
const hpMN: Types.HangingProtocol.Protocol = {
hasUpdatedPriorsInformation: false,
id: '@ohif/hp-extension.mn',
description: 'Has various hanging protocol layouts for use in testing',
name: '2x2',
protocolMatchingRules: [
{
id: 'OneOrMoreSeries',
weight: 1,
attribute: 'numberOfDisplaySetsWithImages',
constraint: {
greaterThan: 1,
},
},
],
toolGroupIds: ['default'],
displaySetSelectors: {
defaultDisplaySetId: {
seriesMatchingRules: [
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
},
],
},
},
defaultViewport: {
viewportOptions: {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: -1,
},
],
},
stages: [
{
id: '2x2',
stageActivation: {
enabled: {
minViewportsMatched: 4,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 2,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 2,
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 3,
id: 'defaultDisplaySetId',
},
],
},
],
},
// 3x1 stage
{
id: '3x1',
// Obsolete settings:
requiredViewports: 1,
preferredViewports: 3,
// New equivalent:
stageActivation: {
enabled: {
minViewportsMatched: 3,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
reuseId: '0-0',
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId',
reuseId: '1-0',
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 2,
id: 'defaultDisplaySetId',
reuseId: '0-1',
},
],
},
],
},
// A 2x1 stage
{
id: '2x1',
requiredViewports: 1,
preferredViewports: 2,
stageActivation: {
enabled: {
minViewportsMatched: 2,
},
},
viewportStructure: {
layoutType: 'grid',
layoutType: 'grid',
properties: {
rows: 1,
columns: 2,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
},
],
},
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId',
},
],
},
],
},
// A 1x1 stage - should be automatically activated if there is only 1 viewable instance
{
id: '1x1',
requiredViewports: 1,
preferredViewports: 1,
stageActivation: {
enabled: {
minViewportsMatched: 1,
},
},
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId',
},
],
},
],
},
],
numberOfPriorsReferenced: -1,
};
export default hpMN;
+17
View File
@@ -0,0 +1,17 @@
import hpMN from './hpMN';
const hangingProtocols = [
{
id: '@ohif/hp-extension.mn',
protocol: hpMN,
},
];
/**
* Registers a single study hanging protocol which can be referenced as
* `@ohif/hp-exgtension.mn`, that has initial layouts which show images
* only display sets, up to a 2x2 view.
*/
export default function getHangingProtocolModule() {
return hangingProtocols;
}
+51 -4
View File
@@ -1,17 +1,64 @@
import { id } from './id';
import { Types } from '@ohif/core';
import { id } from './id';
import getHangingProtocolModule from './hp';
// import {setViewportZoomPan, storeViewportZoomPan } from './custom-viewport/setViewportZoomPan';
import sameAs from './custom-attribute/sameAs';
import numberOfDisplaySets from './custom-attribute/numberOfDisplaySets';
import numberOfDisplaySetsWithImages from './custom-attribute/numberOfDisplaySetsWithImages';
import maxNumImageFrames from './custom-attribute/maxNumImageFrames';
import seriesDescriptionsFromDisplaySets from './custom-attribute/seriesDescriptionsFromDisplaySets';
/**
*
* The test extension provides additional behaviour for testing various
* customizations and settings for OHIF.
*/
const testExtension: Types.Extensions.Extension = {
/**
* Only required property. Should be a unique value across all extensions.
*/
id,
preRegistration() {
console.debug('hello from test-extension init.js');
/** Register additional behaviour:
* * HP custom attribute seriesDescriptions to retrieve an array of all series descriptions
* * HP custom attribute numberOfDisplaySets to retrieve the number of display sets
* * HP custom attribute numberOfDisplaySetsWithImages to retrieve the number of display sets containing images
* * HP custom attribute to return a boolean true, when the attribute sameAttribute has the same
* value as another series description in an already matched display set selector named with the value
* in `sameDisplaySetId`
*/
preRegistration: ({ servicesManager }: Types.Extensions.ExtensionParams) => {
const { hangingProtocolService } = servicesManager.services;
hangingProtocolService.addCustomAttribute(
'seriesDescriptions',
'Series Descriptions',
seriesDescriptionsFromDisplaySets
);
hangingProtocolService.addCustomAttribute(
'numberOfDisplaySets',
'Number of displays sets',
numberOfDisplaySets
);
hangingProtocolService.addCustomAttribute(
'numberOfDisplaySetsWithImages',
'Number of displays sets with images',
numberOfDisplaySetsWithImages
);
hangingProtocolService.addCustomAttribute(
'maxNumImageFrames',
'Maximum of number of image frames',
maxNumImageFrames
);
hangingProtocolService.addCustomAttribute(
'sameAs',
'Match an attribute in an existing display set',
sameAs
);
},
/** Registers some additional hanging protocols. See hp/index.tsx for more details */
getHangingProtocolModule,
};
export default testExtension;
@@ -32,7 +32,6 @@ const ptCT = {
ctDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
@@ -42,7 +41,6 @@ const ptCT = {
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
@@ -75,7 +73,6 @@ const ptCT = {
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
@@ -105,7 +102,6 @@ const ptCT = {
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
name: 'default',
viewportStructure: {
layoutType: 'grid',