feat: explicit layout apply via HangingProtocolService (#2936)
* feat: add more types to HP service * change hp to have apply protocol * feat: mpr initial work * apply protocol by id * feat: mode should be able to apply protocol directly * fix: wrong inheritance for viewports drag and drop * apply review comments
This commit is contained in:
parent
fb9744a817
commit
5f480ff746
@ -22,6 +22,20 @@ function areEqual(prevProps, nextProps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.viewportOptions.orientation !==
|
||||
nextProps.viewportOptions.orientation
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.viewportOptions.viewportType !==
|
||||
nextProps.viewportOptions.viewportType
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prevDisplaySets = prevProps.displaySets[0];
|
||||
const nextDisplaySets = nextProps.displaySets[0];
|
||||
|
||||
|
||||
@ -258,8 +258,14 @@ function _getInstanceNumberFromVolume(
|
||||
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
||||
|
||||
if (isAcquisitionPlane) {
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { instanceNumber } =
|
||||
metaData.get('generalImageModule', imageIds[imageIndex]) || {};
|
||||
metaData.get('generalImageModule', imageId) || {};
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ import {
|
||||
segmentation,
|
||||
utilities as csToolsUtils,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import { Types } from '@ohif/core';
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
||||
@ -26,6 +28,9 @@ const commandsModule = ({ servicesManager }) => {
|
||||
UIDialogService,
|
||||
CornerstoneViewportService,
|
||||
SegmentationService,
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
UINotificationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
function _getActiveViewportEnabledElement() {
|
||||
@ -415,6 +420,9 @@ const commandsModule = ({ servicesManager }) => {
|
||||
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
||||
ViewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||
},
|
||||
setHangingProtocol: ({ protocolId }) => {
|
||||
HangingProtocolService.setProtocol(protocolId);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -537,6 +545,11 @@ const commandsModule = ({ servicesManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setHangingProtocol: {
|
||||
commandFn: actions.setHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
146
extensions/cornerstone/src/getHangingProtocolModule.ts
Normal file
146
extensions/cornerstone/src/getHangingProtocolModule.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const MPRHangingProtocolGenerator: Types.HangingProtocol.ProtocolGenerator = ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) => {
|
||||
const {
|
||||
ViewportGridService,
|
||||
UINotificationService,
|
||||
DisplaySetService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { activeViewportIndex, viewports } = ViewportGridService.getState();
|
||||
const viewportDisplaySetInstanceUIDs =
|
||||
viewports[activeViewportIndex].displaySetInstanceUIDs;
|
||||
|
||||
if (
|
||||
!viewportDisplaySetInstanceUIDs ||
|
||||
!viewportDisplaySetInstanceUIDs.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySetsToHang = viewportDisplaySetInstanceUIDs.map(
|
||||
displaySetInstanceUID => {
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
);
|
||||
|
||||
if (displaySetsToHang.some(ds => !ds.isReconstructable)) {
|
||||
UINotificationService.show({
|
||||
title: 'Multiplanar reconstruction (MPR) ',
|
||||
message:
|
||||
'Cannot create MPR for this series since it is not reconstructable.',
|
||||
type: 'warning',
|
||||
displayTime: 3000,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDisplaySets = {};
|
||||
|
||||
displaySetsToHang.forEach(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = displaySet;
|
||||
|
||||
matchingDisplaySets[displaySetInstanceUID] = {
|
||||
displaySetInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} as Types.HangingProtocol.DisplaySetMatchDetails;
|
||||
});
|
||||
|
||||
const hpViewports: Types.HangingProtocol.Viewport[] = [
|
||||
'axial',
|
||||
'sagittal',
|
||||
'coronal',
|
||||
].map(viewportOrientation => {
|
||||
return {
|
||||
viewportOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOrientation,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: viewportDisplaySetInstanceUIDs.map(displaySetInstanceUID => {
|
||||
return {
|
||||
id: displaySetInstanceUID,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const protocol = {
|
||||
id: 'mpr',
|
||||
stages: [
|
||||
{
|
||||
id: 'mprStage',
|
||||
name: 'mpr',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
layoutOptions: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
x: 1 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
x: 2 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
displaySets: [],
|
||||
viewports: hpViewports,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
protocol,
|
||||
matchingDisplaySets,
|
||||
};
|
||||
};
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
id: 'mpr',
|
||||
protocol: MPRHangingProtocolGenerator,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getHangingProtocolModule;
|
||||
@ -9,6 +9,7 @@ import {
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import init from './init.js';
|
||||
import commandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import ToolGroupService from './services/ToolGroupService';
|
||||
import SyncGroupService from './services/SyncGroupService';
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
@ -70,6 +71,7 @@ const cornerstoneExtension = {
|
||||
servicesManager.registerService(SyncGroupService(servicesManager));
|
||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||
},
|
||||
getHangingProtocolModule,
|
||||
getViewportModule({ servicesManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
// const onNewImageHandler = jumpData => {
|
||||
|
||||
@ -14,7 +14,7 @@ const defaultProtocol = {
|
||||
id: 'hYbmMy3b7pz7GLiaT',
|
||||
name: 'default',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 1,
|
||||
@ -56,8 +56,8 @@ const defaultProtocol = {
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: 'default',
|
||||
protocols: [defaultProtocol],
|
||||
id: defaultProtocol.id,
|
||||
protocol: defaultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -2,9 +2,9 @@ import getDataSourcesModule from './getDataSourcesModule.js';
|
||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule.js';
|
||||
import getToolbarModule from './getToolbarModule';
|
||||
import commandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||
import { id } from './id.js';
|
||||
import init from './init';
|
||||
@ -18,9 +18,9 @@ const defaultExtension = {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getDataSourcesModule,
|
||||
getHangingProtocolModule,
|
||||
getLayoutTemplateModule,
|
||||
getPanelModule,
|
||||
getHangingProtocolModule,
|
||||
getSopClassHandlerModule,
|
||||
getToolbarModule,
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
const ptCT = {
|
||||
id: 'tmtv-pt-ct',
|
||||
id: 'ptCT',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
@ -15,6 +15,15 @@ const ptCT = {
|
||||
],
|
||||
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
id: 'wauZK2QNEfDPwcAQo',
|
||||
weight: 1,
|
||||
attribute: 'ModalitiesInStudy',
|
||||
constraint: {
|
||||
contains: ['CT', 'PT'],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'wauZK2QNEfDPwcAQo',
|
||||
weight: 1,
|
||||
@ -632,8 +641,8 @@ const ptCT = {
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: 'ptCT',
|
||||
protocols: [ptCT],
|
||||
id: ptCT.id,
|
||||
protocol: ptCT,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default',
|
||||
};
|
||||
|
||||
const tracked = {
|
||||
@ -103,6 +102,7 @@ function modeFactory() {
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'MPR',
|
||||
'MoreTools',
|
||||
]);
|
||||
},
|
||||
@ -169,7 +169,8 @@ function modeFactory() {
|
||||
},
|
||||
],
|
||||
extensions: extensionDependencies,
|
||||
hangingProtocols: [ohif.hangingProtocols],
|
||||
// Default protocol gets self-registered by default in the init
|
||||
hangingProtocol: 'default',
|
||||
// Order is important in sop class handlers when two handlers both use
|
||||
// the same sop class under different situations. In that case, the more
|
||||
// general handler needs to come last. For this case, the dicomvideo must
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
function initDefaultToolGroup(
|
||||
extensionManager,
|
||||
ToolGroupService,
|
||||
commandsManager
|
||||
commandsManager,
|
||||
toolGroupId
|
||||
) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
@ -57,7 +58,6 @@ function initDefaultToolGroup(
|
||||
},
|
||||
};
|
||||
|
||||
const toolGroupId = 'default';
|
||||
ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig);
|
||||
}
|
||||
|
||||
@ -140,8 +140,19 @@ function initSRToolGroup(extensionManager, ToolGroupService, commandsManager) {
|
||||
}
|
||||
|
||||
function initToolGroups(extensionManager, ToolGroupService, commandsManager) {
|
||||
initDefaultToolGroup(extensionManager, ToolGroupService, commandsManager);
|
||||
initDefaultToolGroup(
|
||||
extensionManager,
|
||||
ToolGroupService,
|
||||
commandsManager,
|
||||
'default'
|
||||
);
|
||||
initSRToolGroup(extensionManager, ToolGroupService, commandsManager);
|
||||
initDefaultToolGroup(
|
||||
extensionManager,
|
||||
ToolGroupService,
|
||||
commandsManager,
|
||||
'mpr'
|
||||
);
|
||||
}
|
||||
|
||||
export default initToolGroups;
|
||||
|
||||
@ -290,6 +290,23 @@ const toolbarButtons = [
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
// Todo: MPR not ready yet for SEG support, not activating it now
|
||||
{
|
||||
id: 'MPR',
|
||||
type: 'ohif.action',
|
||||
props: {
|
||||
icon: 'old-play',
|
||||
label: 'MPR',
|
||||
type: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: { protocolId: 'mpr' },
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// More...
|
||||
{
|
||||
id: 'MoreTools',
|
||||
|
||||
@ -8,7 +8,6 @@ import setFusionActiveVolume from './utils/setFusionActiveVolume.js';
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default',
|
||||
measurements: '@ohif/extension-default.panelModule.measure',
|
||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||
};
|
||||
@ -188,7 +187,7 @@ function modeFactory({ modeConfiguration }) {
|
||||
},
|
||||
],
|
||||
extensions: extensionDependencies,
|
||||
hangingProtocols: [tmtv.hangingProtocols],
|
||||
hangingProtocol: 'ptCT',
|
||||
sopClassHandlers: [ohif.sopClassHandler],
|
||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||
};
|
||||
|
||||
@ -43,12 +43,10 @@ export default class ExtensionManager {
|
||||
const {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
} = _servicesManager.services;
|
||||
|
||||
MeasurementService.clearMeasurements();
|
||||
ViewportGridService.reset();
|
||||
HangingProtocolService.reset();
|
||||
|
||||
registeredExtensionIds.forEach(extensionId => {
|
||||
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
||||
@ -74,12 +72,10 @@ export default class ExtensionManager {
|
||||
const {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
} = _servicesManager.services;
|
||||
|
||||
MeasurementService.clearMeasurements();
|
||||
ViewportGridService.reset();
|
||||
HangingProtocolService.reset();
|
||||
|
||||
registeredExtensionIds.forEach(extensionId => {
|
||||
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
||||
@ -196,6 +192,8 @@ export default class ExtensionManager {
|
||||
dataSources
|
||||
);
|
||||
break;
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
this._initHangingProtocolsModule(extensionModule, extensionId);
|
||||
case MODULE_TYPES.TOOLBAR:
|
||||
case MODULE_TYPES.VIEWPORT:
|
||||
case MODULE_TYPES.PANEL:
|
||||
@ -203,7 +201,6 @@ export default class ExtensionManager {
|
||||
case MODULE_TYPES.CONTEXT:
|
||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||
case MODULE_TYPES.UTILITY:
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
// Default for most extension points,
|
||||
// Just adds each entry ready for consumption by mode.
|
||||
extensionModule.forEach(element => {
|
||||
@ -287,6 +284,13 @@ export default class ExtensionManager {
|
||||
}
|
||||
};
|
||||
|
||||
_initHangingProtocolsModule = (extensionModule, extensionId) => {
|
||||
const { HangingProtocolService } = this._servicesManager.services;
|
||||
extensionModule.forEach(({ id, protocol }) => {
|
||||
HangingProtocolService.addProtocol(id, protocol);
|
||||
});
|
||||
};
|
||||
|
||||
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
|
||||
const { UserAuthenticationService } = this._servicesManager.services;
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ describe('ExtensionManager.js', () => {
|
||||
services: {
|
||||
// Required for DataSource Module initiation
|
||||
UserAuthenticationService: jest.fn(),
|
||||
HangingProtocolService: {
|
||||
addProtocol: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
appConfig = {
|
||||
|
||||
@ -2,6 +2,7 @@ interface IDisplaySet {
|
||||
displaySetInstanceUID: string;
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID?: string;
|
||||
SeriesNumber?: string;
|
||||
}
|
||||
|
||||
export default IDisplaySet;
|
||||
|
||||
@ -127,7 +127,7 @@ describe('HangingProtocolService', () => {
|
||||
let initialScaling;
|
||||
|
||||
beforeAll(() => {
|
||||
hps.addProtocols([testProtocol]);
|
||||
hps.addProtocol(testProtocol.id, testProtocol);
|
||||
});
|
||||
|
||||
it('has one protocol', () => {
|
||||
@ -137,11 +137,7 @@ describe('HangingProtocolService', () => {
|
||||
describe('run', () => {
|
||||
it('matches best image match', () => {
|
||||
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
||||
const {
|
||||
hpAlreadyApplied,
|
||||
viewportMatchDetails,
|
||||
displaySetMatchDetails,
|
||||
} = hps.getMatchDetails();
|
||||
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails();
|
||||
expect(hpAlreadyApplied).toMatchObject([false]);
|
||||
expect(viewportMatchDetails.length).toBe(1);
|
||||
expect(viewportMatchDetails[0]).toMatchObject({
|
||||
|
||||
@ -3,6 +3,7 @@ import sortBy from '../../utils/sortBy';
|
||||
import ProtocolEngine from './ProtocolEngine';
|
||||
import StudyMetadata from '../../types/StudyMetadata';
|
||||
import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
||||
import { HangingProtocol } from '../../types';
|
||||
|
||||
const EVENTS = {
|
||||
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
||||
@ -12,51 +13,24 @@ const EVENTS = {
|
||||
'event::hanging_protocol_custom_image_load_performed',
|
||||
};
|
||||
|
||||
type ViewportOptions = {
|
||||
orientation: string;
|
||||
toolGroupId: string;
|
||||
viewportId: string;
|
||||
viewportType: string;
|
||||
initialImageOptions: Record<string, unknown>;
|
||||
syncGroups: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ViewportMatchDetails = {
|
||||
viewportOptions: ViewportOptions;
|
||||
displaySetsInfo: {
|
||||
SeriesInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
displaySetOptions: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
type DisplaySetMatchDetails = {
|
||||
SeriesInstanceUID: string;
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
matchDetails: any;
|
||||
matchingScore: number;
|
||||
sortingInfo: any;
|
||||
};
|
||||
|
||||
type HangingProtocolMatchDetails = {
|
||||
displaySetMatchDetails: Map<string, DisplaySetMatchDetails>;
|
||||
viewportMatchDetails: ViewportMatchDetails[];
|
||||
hpAlreadyApplied: boolean[];
|
||||
};
|
||||
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
|
||||
|
||||
class HangingProtocolService {
|
||||
studies: StudyMetadata[];
|
||||
protocols: Record<string, unknown>[];
|
||||
protocol: Record<string, unknown>;
|
||||
// stores all the protocols (object or function that returns an object) in a map
|
||||
protocols: Map<string, Protocol>;
|
||||
// the current protocol that is being applied to the viewports in object format
|
||||
protocol: HangingProtocol.Protocol;
|
||||
stage: number;
|
||||
_commandsManager: Record<string, unknown>;
|
||||
_servicesManager: Record<string, unknown>;
|
||||
protocolEngine: ProtocolEngine;
|
||||
hpAlreadyApplied: boolean[] = [];
|
||||
customViewportSettings = [];
|
||||
displaySets: IDisplaySet[] = [];
|
||||
activeStudy: Record<string, unknown>;
|
||||
debugLogging: false;
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
customAttributeRetrievalCallbacks = {
|
||||
NumberOfStudyRelatedSeries: {
|
||||
@ -89,17 +63,21 @@ class HangingProtocolService {
|
||||
* DisplaySetId is the id defined in the hangingProtocol object itself
|
||||
* and match is an object that contains information about
|
||||
*/
|
||||
displaySetMatchDetails: Map<string, DisplaySetMatchDetails> = new Map();
|
||||
displaySetMatchDetails: Map<
|
||||
string,
|
||||
HangingProtocol.DisplaySetMatchDetails
|
||||
> = new Map();
|
||||
|
||||
/**
|
||||
* An array that contains for each viewport (viewportIndex) specified in the
|
||||
* hanging protocol, an object of the form
|
||||
*/
|
||||
viewportMatchDetails = [] as ViewportMatchDetails[];
|
||||
viewportMatchDetails = [] as HangingProtocol.ViewportMatchDetails[];
|
||||
|
||||
constructor(commandsManager) {
|
||||
constructor(commandsManager, servicesManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
this.protocols = [];
|
||||
this._servicesManager = servicesManager;
|
||||
this.protocols = new Map();
|
||||
this.protocolEngine = undefined;
|
||||
this.protocol = undefined;
|
||||
this.stage = undefined;
|
||||
@ -116,13 +94,17 @@ class HangingProtocolService {
|
||||
|
||||
public reset() {
|
||||
this.studies = [];
|
||||
this.protocols = [];
|
||||
this.protocols = new Map();
|
||||
this.hpAlreadyApplied = [];
|
||||
this.viewportMatchDetails = [];
|
||||
// this.ProtocolEngine.reset()
|
||||
}
|
||||
|
||||
public getMatchDetails(): HangingProtocolMatchDetails {
|
||||
public getDefaultProtocol(): HangingProtocol.Protocol {
|
||||
return this.getProtocolById('default');
|
||||
}
|
||||
|
||||
public getMatchDetails(): HangingProtocol.HangingProtocolMatchDetails {
|
||||
return {
|
||||
viewportMatchDetails: this.viewportMatchDetails,
|
||||
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||
@ -130,16 +112,76 @@ class HangingProtocolService {
|
||||
};
|
||||
}
|
||||
|
||||
public getProtocols() {
|
||||
return this.protocols;
|
||||
/**
|
||||
* It loops over the protocols map object, and checks whether the protocol
|
||||
* is a function, if so, it executes it and returns the result as a protocol object
|
||||
* otherwise it returns the protocol object itself
|
||||
*
|
||||
* @returns all the hanging protocol registered in the HangingProtocolService
|
||||
*/
|
||||
public getProtocols(): HangingProtocol.Protocol[] {
|
||||
// this.protocols is a map of protocols with the protocol id as the key
|
||||
// and the protocol or a function that returns a protocol as the value
|
||||
const protocols = [];
|
||||
// @ts-ignore
|
||||
for (const protocolId of this.protocols.keys()) {
|
||||
const protocol = this.getProtocolById(protocolId);
|
||||
if (protocol) {
|
||||
protocols.push(protocol);
|
||||
}
|
||||
}
|
||||
|
||||
return protocols;
|
||||
}
|
||||
|
||||
public addProtocols(protocols) {
|
||||
protocols.forEach(protocol => {
|
||||
if (this.protocols.indexOf(protocol) === -1) {
|
||||
this.protocols.push(this._validateProtocol(protocol));
|
||||
/**
|
||||
* Returns the protocol with the given id, it will get the protocol from the
|
||||
* protocols map object and if it is a function, it will execute it and return
|
||||
* the result as a protocol object
|
||||
*
|
||||
* @param protocolId - the id of the protocol
|
||||
* @returns protocol - the protocol with the given id
|
||||
*/
|
||||
public getProtocolById(id: string): HangingProtocol.Protocol {
|
||||
const protocol = this.protocols.get(id);
|
||||
|
||||
if (protocol instanceof Function) {
|
||||
try {
|
||||
const { protocol: generatedProtocol } = this._getProtocolFromGenerator(
|
||||
protocol
|
||||
);
|
||||
|
||||
return generatedProtocol;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Error while executing protocol generator for protocol ${id}: ${error}`
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It adds a protocol to the protocols map object. If a protocol with the given
|
||||
* id already exists, warn the user and overwrite it.
|
||||
*
|
||||
* @param {string} protocolId - The id of the protocol.
|
||||
* @param {Protocol} protocol - Protocol - This is the protocol that you want to
|
||||
* add to the protocol manager.
|
||||
*/
|
||||
public addProtocol(protocolId: string, protocol: Protocol): void {
|
||||
if (this.protocols.has(protocolId)) {
|
||||
console.warn(
|
||||
`A protocol with id ${protocolId} already exists. It will be overwritten.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!(protocol instanceof Function)) {
|
||||
protocol = this._validateProtocol(protocol as HangingProtocol.Protocol);
|
||||
}
|
||||
|
||||
this.protocols.set(protocolId, protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,28 +198,28 @@ class HangingProtocolService {
|
||||
* @param protocol is a specific protocol to apply.
|
||||
* @returns
|
||||
*/
|
||||
public run({ studies, displaySets, activeStudy }, protocol) {
|
||||
public run({ studies, displaySets, activeStudy }, protocolId) {
|
||||
this.studies = [...studies];
|
||||
this.displaySets = displaySets;
|
||||
this.activeStudy = activeStudy || studies[0];
|
||||
|
||||
this.protocolEngine = new ProtocolEngine(
|
||||
this.protocols,
|
||||
this.getProtocols(),
|
||||
this.customAttributeRetrievalCallbacks
|
||||
);
|
||||
|
||||
// if there is no pre-defined protocol
|
||||
if (!protocol || protocol.id === undefined) {
|
||||
const matchedProtocol = this.protocolEngine.run({
|
||||
studies: this.studies,
|
||||
activeStudy,
|
||||
displaySets,
|
||||
});
|
||||
this._setProtocol(matchedProtocol);
|
||||
if (protocolId) {
|
||||
const protocol = this.getProtocolById(protocolId);
|
||||
this._setProtocol(protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setProtocol(protocol);
|
||||
const matchedProtocol = this.protocolEngine.run({
|
||||
studies: this.studies,
|
||||
activeStudy,
|
||||
displaySets,
|
||||
});
|
||||
this._setProtocol(matchedProtocol);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +227,7 @@ class HangingProtocolService {
|
||||
* and its callback has been added to the HangingProtocolService
|
||||
* @returns {boolean} true
|
||||
*/
|
||||
public hasCustomImageLoadStrategy() {
|
||||
public hasCustomImageLoadStrategy(): boolean {
|
||||
return (
|
||||
this.activeImageLoadStrategyName !== null &&
|
||||
this.registeredImageLoadStrategies[
|
||||
@ -194,7 +236,7 @@ class HangingProtocolService {
|
||||
);
|
||||
}
|
||||
|
||||
public getCustomImageLoadPerformed() {
|
||||
public getCustomImageLoadPerformed(): boolean {
|
||||
return this.customImageLoadPerformed;
|
||||
}
|
||||
|
||||
@ -203,13 +245,13 @@ class HangingProtocolService {
|
||||
* @param {string} name strategy name
|
||||
* @param {Function} callback image loader callback
|
||||
*/
|
||||
public registerImageLoadStrategy(name, callback) {
|
||||
public registerImageLoadStrategy(name, callback): void {
|
||||
if (callback instanceof Function && name) {
|
||||
this.registeredImageLoadStrategies[name] = callback;
|
||||
}
|
||||
}
|
||||
|
||||
public setHangingProtocolAppliedForViewport(i) {
|
||||
public setHangingProtocolAppliedForViewport(i): void {
|
||||
this.hpAlreadyApplied[i] = true;
|
||||
}
|
||||
|
||||
@ -222,7 +264,12 @@ class HangingProtocolService {
|
||||
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
|
||||
* @param options to add to the "this" object for the custom attribute retriever
|
||||
*/
|
||||
public addCustomAttribute(attributeId, attributeName, callback, options) {
|
||||
public addCustomAttribute(
|
||||
attributeId: string,
|
||||
attributeName: string,
|
||||
callback: (metadata: any) => any,
|
||||
options: Record<string, any> = {}
|
||||
): void {
|
||||
this.customAttributeRetrievalCallbacks[attributeId] = {
|
||||
...options,
|
||||
id: attributeId,
|
||||
@ -234,7 +281,7 @@ class HangingProtocolService {
|
||||
/**
|
||||
* Switches to the next protocol stage in the display set sequence
|
||||
*/
|
||||
public nextProtocolStage() {
|
||||
public nextProtocolStage(): void {
|
||||
console.log('ProtocolEngine::nextProtocolStage');
|
||||
|
||||
if (!this._setCurrentProtocolStage(1)) {
|
||||
@ -245,7 +292,7 @@ class HangingProtocolService {
|
||||
/**
|
||||
* Switches to the previous protocol stage in the display set sequence
|
||||
*/
|
||||
public previousProtocolStage() {
|
||||
public previousProtocolStage(): void {
|
||||
console.log('ProtocolEngine::previousProtocolStage');
|
||||
|
||||
if (!this._setCurrentProtocolStage(-1)) {
|
||||
@ -257,7 +304,7 @@ class HangingProtocolService {
|
||||
* Executes the callback function for the custom loading strategy for the images
|
||||
* if no strategy is set, the default strategy is used
|
||||
*/
|
||||
runImageLoadStrategy(data) {
|
||||
runImageLoadStrategy(data): void {
|
||||
const loader = this.registeredImageLoadStrategies[
|
||||
this.activeImageLoadStrategyName
|
||||
];
|
||||
@ -277,8 +324,14 @@ class HangingProtocolService {
|
||||
this._broadcastChange(this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, loadedData);
|
||||
}
|
||||
|
||||
_validateProtocol(protocol) {
|
||||
_validateProtocol(
|
||||
protocol: HangingProtocol.Protocol
|
||||
): HangingProtocol.Protocol {
|
||||
protocol.id = protocol.id || protocol.name;
|
||||
const defaultViewportOptions = {
|
||||
toolGroupId: 'default',
|
||||
viewportType: 'stack',
|
||||
};
|
||||
// Automatically compute some number of attributes if they
|
||||
// aren't present. Makes defining new HPs easier.
|
||||
protocol.name = protocol.name || protocol.id;
|
||||
@ -292,13 +345,14 @@ class HangingProtocolService {
|
||||
|
||||
for (let i = 0; i < rows * columns; i++) {
|
||||
stage.viewports.push({
|
||||
viewportOptions: {},
|
||||
viewportOptions: defaultViewportOptions,
|
||||
displaySets: [],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stage.viewports.forEach(viewport => {
|
||||
viewport.viewportOptions = viewport.viewportOptions || {};
|
||||
viewport.viewportOptions =
|
||||
viewport.viewportOptions || defaultViewportOptions;
|
||||
if (!viewport.displaySets) {
|
||||
viewport.displaySets = [];
|
||||
} else {
|
||||
@ -313,9 +367,53 @@ class HangingProtocolService {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
_setProtocol(protocol) {
|
||||
// TODO: Add proper Protocol class to validate the protocols
|
||||
// which are entered manually
|
||||
/**
|
||||
* It applied the protocol to the current studies and display sets based on the
|
||||
* protocolId that is provided.
|
||||
* @param protocolId - name of the protocol to be set
|
||||
* @param protocol - protocol object (optional), if not provided, the protocol
|
||||
* will be retrieved from the list of protocols by its name
|
||||
* @param matchingDisplaySets - predefined display sets to be used for the protocol
|
||||
*/
|
||||
public setProtocol(
|
||||
protocolId: string,
|
||||
protocol?: HangingProtocol.Protocol,
|
||||
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
|
||||
): void {
|
||||
if (!protocol) {
|
||||
const foundProtocol = this.protocols.get(protocolId);
|
||||
|
||||
if (!foundProtocol) {
|
||||
console.warn(
|
||||
`HangingProtocolService::setProtocol - protocol ${protocolId} not found`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (foundProtocol instanceof Function) {
|
||||
try {
|
||||
({ protocol, matchingDisplaySets } = this._getProtocolFromGenerator(
|
||||
foundProtocol
|
||||
));
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`HangingProtocolService::setProtocol - protocol ${protocolId} failed to execute`,
|
||||
error
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
protocol = foundProtocol;
|
||||
}
|
||||
}
|
||||
|
||||
this._setProtocol(protocol, matchingDisplaySets);
|
||||
}
|
||||
|
||||
private _setProtocol(
|
||||
protocol: HangingProtocol.Protocol,
|
||||
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
|
||||
): void {
|
||||
this.stage = 0;
|
||||
this.protocol = protocol;
|
||||
const { imageLoadStrategy } = protocol;
|
||||
@ -328,7 +426,7 @@ class HangingProtocolService {
|
||||
this.activeImageLoadStrategyName = imageLoadStrategy;
|
||||
}
|
||||
}
|
||||
this._updateViewports();
|
||||
this._updateViewports(matchingDisplaySets);
|
||||
|
||||
this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, {
|
||||
viewportMatchDetails: this.viewportMatchDetails,
|
||||
@ -362,19 +460,50 @@ class HangingProtocolService {
|
||||
return this.protocol.stages[this.stage];
|
||||
}
|
||||
|
||||
private _getProtocolFromGenerator(
|
||||
protocolGenerator: HangingProtocol.ProtocolGenerator
|
||||
): {
|
||||
protocol: HangingProtocol.Protocol;
|
||||
matchingDisplaySets: Record<string, HangingProtocol.DisplaySetMatchDetails>;
|
||||
} {
|
||||
const { protocol, matchingDisplaySets } = protocolGenerator({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
});
|
||||
|
||||
const validatedProtocol = this._validateProtocol(protocol);
|
||||
|
||||
return {
|
||||
protocol: validatedProtocol,
|
||||
matchingDisplaySets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the viewports with the selected protocol stage.
|
||||
*/
|
||||
_updateViewports() {
|
||||
_updateViewports(
|
||||
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
|
||||
): void {
|
||||
// Make sure we have an active protocol with a non-empty array of display sets
|
||||
if (!this._getNumProtocolStages()) {
|
||||
console.log('No protocol stages - nothing to display');
|
||||
return;
|
||||
}
|
||||
|
||||
// each time we are updating the viewports, we need to reset the
|
||||
// matching applied
|
||||
this.hpAlreadyApplied = [];
|
||||
|
||||
// reset displaySetMatchDetails
|
||||
this.displaySetMatchDetails = new Map();
|
||||
|
||||
if (matchingDisplaySets) {
|
||||
this.displaySetMatchDetails = new Map(
|
||||
Object.entries(matchingDisplaySets)
|
||||
);
|
||||
}
|
||||
|
||||
// Retrieve the current stage
|
||||
const stageModel = this._getCurrentStageModel();
|
||||
|
||||
@ -392,7 +521,7 @@ class HangingProtocolService {
|
||||
}
|
||||
|
||||
this.customImageLoadPerformed = false;
|
||||
const { type: layoutType } = stageModel.viewportStructure;
|
||||
const { layoutType } = stageModel.viewportStructure;
|
||||
|
||||
// Retrieve the properties associated with the current display set's viewport structure template
|
||||
// If no such layout properties exist, stop here.
|
||||
@ -412,14 +541,19 @@ class HangingProtocolService {
|
||||
});
|
||||
|
||||
// Matching the displaySets
|
||||
for (const displaySet of stageModel.displaySets) {
|
||||
// skip matching if already matched
|
||||
if (this.displaySetMatchDetails.has(displaySet.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stageModel.displaySets.forEach(displaySet => {
|
||||
const { bestMatch, matchingScores } = this._matchImages(displaySet);
|
||||
this.displaySetMatchDetails.set(displaySet.id, bestMatch);
|
||||
|
||||
if (bestMatch) {
|
||||
bestMatch.matchingScores = matchingScores;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Loop through each viewport
|
||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
@ -430,6 +564,8 @@ class HangingProtocolService {
|
||||
// but it is a info to locate the displaySet from the displaySetService
|
||||
const displaySetsInfo = [];
|
||||
viewport.displaySets.forEach(
|
||||
// Todo: why do we have displaySetIndex here? It is not used in the protocol
|
||||
// definition
|
||||
({ id, displaySetIndex = 0, options: displaySetOptions }) => {
|
||||
const viewportDisplaySetMain = this.displaySetMatchDetails.get(id);
|
||||
// Use the display set index to allow getting the "next" match, eg
|
||||
@ -445,7 +581,7 @@ class HangingProtocolService {
|
||||
displaySetInstanceUID,
|
||||
} = viewportDisplaySet;
|
||||
|
||||
const displaySetInfo = {
|
||||
const displaySetInfo: HangingProtocol.DisplaySetInfo = {
|
||||
SeriesInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
displaySetOptions,
|
||||
@ -524,6 +660,7 @@ class HangingProtocolService {
|
||||
const seriesMatchDetails = this.protocolEngine.findMatch(
|
||||
displaySet,
|
||||
seriesMatchingRules,
|
||||
// Todo: why we have images here since the matching type does not have it
|
||||
{ studies: this.studies, instance: displaySet.images?.[0] }
|
||||
);
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import HangingProtocolService from './HangingProtocolService';
|
||||
|
||||
export default {
|
||||
name: 'HangingProtocolService',
|
||||
create: ({ configuration = {}, commandsManager }) => {
|
||||
return new HangingProtocolService(commandsManager);
|
||||
create: ({ configuration = {}, commandsManager, servicesManager }) => {
|
||||
return new HangingProtocolService(commandsManager, servicesManager);
|
||||
},
|
||||
};
|
||||
|
||||
@ -37,6 +37,7 @@ export default class ServicesManager {
|
||||
this.services[service.name] = service.create({
|
||||
configuration,
|
||||
commandsManager: this._commandsManager,
|
||||
servicesManager: this,
|
||||
});
|
||||
} else {
|
||||
log.warn(`Service create factory function not defined. Exiting early.`);
|
||||
|
||||
@ -4,10 +4,15 @@ import log from '../log.js';
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('ServicesManager.js', () => {
|
||||
let servicesManager;
|
||||
let servicesManager, commandsManager;
|
||||
|
||||
beforeEach(() => {
|
||||
servicesManager = new ServicesManager();
|
||||
commandsManager = {
|
||||
createContext: jest.fn(),
|
||||
getContext: jest.fn(),
|
||||
registerCommand: jest.fn(),
|
||||
};
|
||||
servicesManager = new ServicesManager(commandsManager);
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@ -90,9 +95,9 @@ describe('ServicesManager.js', () => {
|
||||
|
||||
servicesManager.registerService(fakeService, configuration);
|
||||
|
||||
expect(fakeService.create.mock.calls[0][0]).toEqual({
|
||||
configuration,
|
||||
});
|
||||
expect(fakeService.create.mock.calls[0][0].configuration.config).toBe(
|
||||
configuration.config
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
140
platform/core/src/types/HangingProtocol.ts
Normal file
140
platform/core/src/types/HangingProtocol.ts
Normal file
@ -0,0 +1,140 @@
|
||||
type DisplaySetInfo = {
|
||||
SeriesInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
displaySetOptions: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ViewportMatchDetails = {
|
||||
viewportOptions: ViewportOptions;
|
||||
displaySetsInfo: DisplaySetInfo[];
|
||||
};
|
||||
|
||||
type DisplaySetMatchDetails = {
|
||||
SeriesInstanceUID: string;
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
matchDetails?: any;
|
||||
matchingScores?: any[];
|
||||
sortingInfo?: any;
|
||||
};
|
||||
|
||||
type HangingProtocolMatchDetails = {
|
||||
displaySetMatchDetails: Map<string, DisplaySetMatchDetails>;
|
||||
viewportMatchDetails: ViewportMatchDetails[];
|
||||
hpAlreadyApplied: boolean[];
|
||||
};
|
||||
|
||||
type MatchingRule = {
|
||||
id: string;
|
||||
weight: number;
|
||||
attribute: string;
|
||||
constraint: Record<string, unknown>;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
type ViewportLayoutOptions = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type ViewportStructure = {
|
||||
layoutType: string;
|
||||
properties: {
|
||||
rows: number;
|
||||
columns: number;
|
||||
layoutOptions: ViewportLayoutOptions[];
|
||||
};
|
||||
};
|
||||
|
||||
type DisplaySet = {
|
||||
id: string;
|
||||
imageMatchingRules: MatchingRule[];
|
||||
seriesMatchingRules: MatchingRule[];
|
||||
studyMatchingRules: MatchingRule[];
|
||||
};
|
||||
|
||||
type SyncGroup = {
|
||||
type: string;
|
||||
id: string;
|
||||
source?: boolean
|
||||
target?: boolean
|
||||
}
|
||||
|
||||
type initialImageOptions = {
|
||||
index?: number;
|
||||
preset? : string; // todo: type more
|
||||
}
|
||||
|
||||
type ViewportOptions = {
|
||||
toolGroupId: string;
|
||||
viewportType: string;
|
||||
id?: string;
|
||||
orientation?: string;
|
||||
viewportId?: string;
|
||||
initialImageOptions?: initialImageOptions;
|
||||
syncGroups?: SyncGroup[];
|
||||
customViewportProps? : Record<string, unknown>;
|
||||
};
|
||||
|
||||
type DisplaySetOptions = {
|
||||
id: string;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type Viewport = {
|
||||
viewportOptions: ViewportOptions;
|
||||
displaySets: DisplaySetOptions[];
|
||||
};
|
||||
|
||||
type ProtocolStage = {
|
||||
id: string;
|
||||
name: string;
|
||||
viewportStructure: ViewportStructure;
|
||||
displaySets: DisplaySet[];
|
||||
viewports: Viewport[];
|
||||
createdDate?: string;
|
||||
};
|
||||
|
||||
type Protocol = {
|
||||
// Mandatory
|
||||
id: string;
|
||||
stages: ProtocolStage[];
|
||||
// Optional
|
||||
locked?: boolean;
|
||||
hasUpdatedPriorsInformation?: boolean;
|
||||
name?: string;
|
||||
createdDate?: string;
|
||||
modifiedDate?: string;
|
||||
availableTo?: Record<string, unknown>;
|
||||
editableBy?: Record<string, unknown>;
|
||||
toolGroupIds?: string[];
|
||||
imageLoadStrategy?: string; // Todo: this should be types specifically
|
||||
protocolMatchingRules?: MatchingRule[];
|
||||
numberOfPriorsReferenced?: number;
|
||||
};
|
||||
|
||||
type ProtocolGenerator = ({servicesManager: any, commandsManager: any}) => {
|
||||
protocol: Protocol;
|
||||
matchingDisplaySets: any;
|
||||
};
|
||||
|
||||
export type {
|
||||
ProtocolGenerator,
|
||||
ViewportOptions,
|
||||
ViewportMatchDetails,
|
||||
DisplaySetMatchDetails,
|
||||
HangingProtocolMatchDetails,
|
||||
Protocol,
|
||||
ProtocolStage,
|
||||
Viewport,
|
||||
DisplaySet,
|
||||
ViewportStructure,
|
||||
ViewportLayoutOptions,
|
||||
DisplaySetOptions,
|
||||
MatchingRule,
|
||||
SyncGroup,
|
||||
initialImageOptions,
|
||||
DisplaySetInfo
|
||||
};
|
||||
@ -6,4 +6,12 @@ import {
|
||||
|
||||
import Consumer from './Consumer';
|
||||
|
||||
export { StudyMetadata, SeriesMetadata, InstanceMetadata, Consumer };
|
||||
import * as HangingProtocol from './HangingProtocol';
|
||||
|
||||
export type {
|
||||
HangingProtocol,
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
Consumer,
|
||||
};
|
||||
|
||||
@ -28,7 +28,7 @@ const deafultProtocol = {
|
||||
id: 'hYbmMy3b7pz7GLiaT',
|
||||
name: 'default',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
@ -64,8 +64,8 @@ const deafultProtocol = {
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: hangingProtocolName,
|
||||
protocols: [defaultProtocol],
|
||||
id: 'default',
|
||||
protocol: deafultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -149,8 +149,8 @@ async function defaultRouteInit({
|
||||
});
|
||||
|
||||
DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {
|
||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
HangingProtocolService.run(studyMetadata);
|
||||
const studyMetadata = // get study metadata and displaySets
|
||||
HangingProtocolService.run({studies, displaySets, activeStudy});
|
||||
});
|
||||
|
||||
return unsubscriptions;
|
||||
|
||||
@ -8,20 +8,32 @@ sidebar_label: Hanging Protocol Service
|
||||
## Overview
|
||||
|
||||
`HangingProtocolService` is a migration of the `OHIF-v1` hanging protocol
|
||||
engine. This service handles the arrangement of the images in the viewport. In
|
||||
short, the registered protocols will get matched with the Series that are
|
||||
available for the series. Each protocol gets a point, and they are ranked. The
|
||||
engine along with various improvements and fixes.
|
||||
This service handles the arrangement of the images in the viewport. In
|
||||
short, the registered protocols will get matched with the DisplaySets that are
|
||||
available. Each protocol gets a score, and they are ranked. The
|
||||
winning protocol gets applied and its settings run for the viewports.
|
||||
|
||||
You can read more about hanging protocols
|
||||
[here](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf).
|
||||
In short with `OHIF-v3` hanging protocols you can:
|
||||
In `OHIF-v3` hanging protocols you can:
|
||||
|
||||
- Define what layout of the viewport should the viewer starts with (2x2 layout)
|
||||
- Define which series gets displayed in which position of the layout
|
||||
- Apply certain initial viewport settings; e.g., inverting the contrast
|
||||
- Enable certain tools based on what series are displayed: link prostate T2 and
|
||||
ADC MRI.
|
||||
- Specify the type of the viewport and its orientation
|
||||
- Define which displaySets gets displayed in which viewport of the layout
|
||||
- Apply certain initial viewport settings; e.g., inverting the contrast, jumping to a specific slice, etc.
|
||||
- Add specific synchronization rules for the viewports
|
||||
|
||||
## Protocols
|
||||
|
||||
A protocol can be an object or a function that returns an object (protocol generator).
|
||||
Each protocol can get added to the `HangingProtocolService` by using the `addProtocol` method given
|
||||
an `id` and the protocol itself. As an example, the `default` protocol is an object, while
|
||||
the 'mpr' protocol is a function that returns the protocol (the reason for this is that the
|
||||
`mpr` protocol needs to be generated based on the active viewport).
|
||||
|
||||
All protocols are stored in the `HangingProtocolService` using their `id` as the key, and
|
||||
the protocol itself as the value.
|
||||
|
||||
## Events
|
||||
|
||||
@ -31,20 +43,21 @@ There are two events that get publish in `HangingProtocolService`:
|
||||
| ------------ | -------------------------------------------------------------------- |
|
||||
| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` |
|
||||
| STAGE_CHANGE | Fires when the the stage is changed in the hanging protocols |
|
||||
| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols |
|
||||
|
||||
|
||||
|
||||
## API
|
||||
|
||||
- `getState`: returns an array: `[matchDetails, hpAlreadyApplied]`:
|
||||
- `getMatchDetails`: returns an object which contains the details of the
|
||||
matching for the viewports, displaysets and whether the protocol is
|
||||
applied to the viewport or not
|
||||
|
||||
- `matchDetails`: matching details for the series
|
||||
- `hpAlreadyApplied`: An array which tracks whether HPServices has been
|
||||
applied on each viewport.
|
||||
|
||||
- `addProtocols`: adds provided protocols to the list of registered protocols
|
||||
- `addProtocol`: adds provided protocol to the list of registered protocols
|
||||
for matching
|
||||
|
||||
- `run(studyMetaData, protocol)`: runs the HPService with the provided
|
||||
studyMetaData and optional protocol. If protocol is not given, HP Matching
|
||||
- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided
|
||||
studyMetaData and optional protocolId. If protocol is not given, HP Matching
|
||||
engine will search all the registered protocols for the best matching one
|
||||
based on the constraints.
|
||||
|
||||
@ -52,6 +65,8 @@ There are two events that get publish in `HangingProtocolService`:
|
||||
|
||||
Default initialization of the modes handles running the `HangingProtocolService`
|
||||
|
||||
|
||||
|
||||
## Custom Attribute
|
||||
In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance,
|
||||
if you have assigned a `timepointId` to each study, and you want to match based on it.
|
||||
@ -129,50 +144,3 @@ function modeFactory() {
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Viewport Settings
|
||||
|
||||
You can define custom settings to be applied to each viewport. There are two
|
||||
types of settings:
|
||||
|
||||
- `viewport settings`: Currently we support two viewport settings
|
||||
|
||||
- `voi`: applying an initial `voi` by setting the windowWidth and windowCenter
|
||||
- `inverted`: inverting the viewport color (e.g., for PET images)
|
||||
|
||||
- `props settings`: Running commands after the first render; e.g., enabling the
|
||||
link tool
|
||||
|
||||
Examples of each settings are :
|
||||
|
||||
```js
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
itemId: 'SyncScroll',
|
||||
interactionType: 'toggle',
|
||||
commandName: 'toggleSynchronizer',
|
||||
commandOptions: { toggledState: true },
|
||||
},
|
||||
commandName: 'setToolActive',
|
||||
type: 'props',
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
and
|
||||
|
||||
```js
|
||||
viewportSettings: [
|
||||
{
|
||||
options: {
|
||||
voi: {
|
||||
windowWidth: 400,
|
||||
windowCenter: 40,
|
||||
},
|
||||
},
|
||||
commandName: '',
|
||||
type: 'viewport',
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
@ -58,8 +58,7 @@ async function defaultRouteInit({
|
||||
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||
({ StudyInstanceUID }) => {
|
||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
HangingProtocolService.run(studyMetadata);
|
||||
HangingProtocolService.run({studies, displaySets, activeStudy});
|
||||
}
|
||||
);
|
||||
unsubscriptions.push(seriesAddedUnsubscribe);
|
||||
@ -89,11 +88,6 @@ export default function ModeRoute(/**..**/) {
|
||||
extensionManager.onModeEnter();
|
||||
mode?.onModeEnter({ servicesManager, extensionManager });
|
||||
|
||||
hangingProtocols.forEach(extentionProtocols => {
|
||||
const { protocols } = extensionManager.getModuleEntry(extentionProtocols);
|
||||
HangingProtocolService.addProtocols(protocols);
|
||||
});
|
||||
|
||||
const setupRouteInit = async () => {
|
||||
if (route.init) {
|
||||
return await route.init(/**...**/);
|
||||
|
||||
@ -49,11 +49,18 @@ export function ViewportGridProvider({ children, service }) {
|
||||
case 'SET_DISPLAYSET_FOR_VIEWPORT': {
|
||||
const payload = action.payload;
|
||||
const { viewportIndex, displaySetInstanceUIDs } = payload;
|
||||
const viewport = state.viewports[viewportIndex];
|
||||
const viewportOptions =
|
||||
payload.viewportOptions || viewport.viewportOptions || {};
|
||||
const displaySetOptions = payload.displaySetOptions ||
|
||||
viewport.displaySetOptions || [{}];
|
||||
|
||||
// Note: there should be no inheritance happening at this level,
|
||||
// we can't assume the new displaySet can inherit the previous
|
||||
// displaySet's or viewportOptions at all. For instance, dragging
|
||||
// and dropping a SEG/RT displaySet without any viewportOptions
|
||||
// or displaySetOptions should not inherit the previous displaySet's
|
||||
// which might have been a PDF Viewport. The viewport itself
|
||||
// will deal with inheritance if required. Here is just a simple
|
||||
// provider.
|
||||
const viewportOptions = payload.viewportOptions || {};
|
||||
const displaySetOptions = payload.displaySetOptions || [{}];
|
||||
|
||||
const viewports = state.viewports.slice();
|
||||
|
||||
// merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
|
||||
|
||||
@ -17,7 +17,10 @@ import Compose from './Compose';
|
||||
* @param props.dataSource to read the data from
|
||||
* @returns array of subscriptions to cancel
|
||||
*/
|
||||
function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) {
|
||||
function defaultRouteInit(
|
||||
{ servicesManager, studyInstanceUIDs, dataSource },
|
||||
hangingProtocol
|
||||
) {
|
||||
const {
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
@ -75,8 +78,12 @@ function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) {
|
||||
// study being displayed, and is thus the "active" study.
|
||||
const activeStudy = studies[0];
|
||||
|
||||
// run the hanging protocol matching service on the displaySets
|
||||
HangingProtocolService.run({ studies, activeStudy, displaySets });
|
||||
// run the hanging protocol matching on the displaySets with the predefined
|
||||
// hanging protocol in the mode configuration
|
||||
HangingProtocolService.run(
|
||||
{ studies, activeStudy, displaySets },
|
||||
hangingProtocol
|
||||
);
|
||||
});
|
||||
|
||||
return unsubscriptions;
|
||||
@ -107,12 +114,9 @@ export default function ModeRoute({
|
||||
locationRef.current = location;
|
||||
}
|
||||
|
||||
const {
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
|
||||
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
|
||||
const { extensions, sopClassHandlers, hotkeys, hangingProtocol } = mode;
|
||||
|
||||
if (dataSourceName === undefined) {
|
||||
dataSourceName = extensionManager.defaultDataSourceName;
|
||||
@ -239,34 +243,28 @@ export default function ModeRoute({
|
||||
});
|
||||
mode?.onModeEnter({ servicesManager, extensionManager, commandsManager });
|
||||
|
||||
// Adding hanging protocols of extensions after onModeEnter since
|
||||
// it will reset the protocols
|
||||
hangingProtocols.forEach(extensionProtocols => {
|
||||
const hangingProtocolModule = extensionManager.getModuleEntry(
|
||||
extensionProtocols
|
||||
);
|
||||
|
||||
if (hangingProtocolModule?.protocols) {
|
||||
HangingProtocolService.addProtocols(hangingProtocolModule.protocols);
|
||||
}
|
||||
});
|
||||
|
||||
const setupRouteInit = async () => {
|
||||
if (route.init) {
|
||||
return await route.init({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
hotkeysManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
});
|
||||
return await route.init(
|
||||
{
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
hotkeysManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
},
|
||||
hangingProtocol
|
||||
);
|
||||
}
|
||||
|
||||
return defaultRouteInit({
|
||||
servicesManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
});
|
||||
return defaultRouteInit(
|
||||
{
|
||||
servicesManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
},
|
||||
hangingProtocol
|
||||
);
|
||||
};
|
||||
|
||||
let unsubscriptions;
|
||||
@ -291,7 +289,6 @@ export default function ModeRoute({
|
||||
hotkeysManager,
|
||||
studyInstanceUIDs,
|
||||
refresh,
|
||||
hangingProtocols,
|
||||
]);
|
||||
|
||||
const renderLayoutData = props => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user