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;
|
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 prevDisplaySets = prevProps.displaySets[0];
|
||||||
const nextDisplaySets = nextProps.displaySets[0];
|
const nextDisplaySets = nextProps.displaySets[0];
|
||||||
|
|
||||||
|
|||||||
@ -258,8 +258,14 @@ function _getInstanceNumberFromVolume(
|
|||||||
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
||||||
|
|
||||||
if (isAcquisitionPlane) {
|
if (isAcquisitionPlane) {
|
||||||
|
const imageId = imageIds[imageIndex];
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const { instanceNumber } =
|
const { instanceNumber } =
|
||||||
metaData.get('generalImageModule', imageIds[imageIndex]) || {};
|
metaData.get('generalImageModule', imageId) || {};
|
||||||
return parseInt(instanceNumber);
|
return parseInt(instanceNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ import {
|
|||||||
segmentation,
|
segmentation,
|
||||||
utilities as csToolsUtils,
|
utilities as csToolsUtils,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
import { Types } from '@ohif/core';
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
|
|
||||||
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
||||||
@ -26,6 +28,9 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
UIDialogService,
|
UIDialogService,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
SegmentationService,
|
SegmentationService,
|
||||||
|
DisplaySetService,
|
||||||
|
HangingProtocolService,
|
||||||
|
UINotificationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
function _getActiveViewportEnabledElement() {
|
function _getActiveViewportEnabledElement() {
|
||||||
@ -415,6 +420,9 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
||||||
ViewportGridService.setActiveViewportIndex(nextViewportIndex);
|
ViewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||||
},
|
},
|
||||||
|
setHangingProtocol: ({ protocolId }) => {
|
||||||
|
HangingProtocolService.setProtocol(protocolId);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
@ -537,6 +545,11 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
setHangingProtocol: {
|
||||||
|
commandFn: actions.setHangingProtocol,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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 { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
import init from './init.js';
|
import init from './init.js';
|
||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import ToolGroupService from './services/ToolGroupService';
|
import ToolGroupService from './services/ToolGroupService';
|
||||||
import SyncGroupService from './services/SyncGroupService';
|
import SyncGroupService from './services/SyncGroupService';
|
||||||
import { toolNames } from './initCornerstoneTools';
|
import { toolNames } from './initCornerstoneTools';
|
||||||
@ -70,6 +71,7 @@ const cornerstoneExtension = {
|
|||||||
servicesManager.registerService(SyncGroupService(servicesManager));
|
servicesManager.registerService(SyncGroupService(servicesManager));
|
||||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||||
},
|
},
|
||||||
|
getHangingProtocolModule,
|
||||||
getViewportModule({ servicesManager, commandsManager }) {
|
getViewportModule({ servicesManager, commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
// const onNewImageHandler = jumpData => {
|
// const onNewImageHandler = jumpData => {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const defaultProtocol = {
|
|||||||
id: 'hYbmMy3b7pz7GLiaT',
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
name: 'default',
|
name: 'default',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
type: 'grid',
|
layoutType: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
rows: 1,
|
rows: 1,
|
||||||
columns: 1,
|
columns: 1,
|
||||||
@ -56,8 +56,8 @@ const defaultProtocol = {
|
|||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'default',
|
id: defaultProtocol.id,
|
||||||
protocols: [defaultProtocol],
|
protocol: defaultProtocol,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import getDataSourcesModule from './getDataSourcesModule.js';
|
|||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule.js';
|
|
||||||
import getToolbarModule from './getToolbarModule';
|
import getToolbarModule from './getToolbarModule';
|
||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import init from './init';
|
import init from './init';
|
||||||
@ -18,9 +18,9 @@ const defaultExtension = {
|
|||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
},
|
},
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
getHangingProtocolModule,
|
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
|
getHangingProtocolModule,
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
getToolbarModule,
|
getToolbarModule,
|
||||||
getCommandsModule({ servicesManager, commandsManager }) {
|
getCommandsModule({ servicesManager, commandsManager }) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const ptCT = {
|
const ptCT = {
|
||||||
id: 'tmtv-pt-ct',
|
id: 'ptCT',
|
||||||
locked: true,
|
locked: true,
|
||||||
hasUpdatedPriorsInformation: false,
|
hasUpdatedPriorsInformation: false,
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
@ -15,6 +15,15 @@ const ptCT = {
|
|||||||
],
|
],
|
||||||
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||||
protocolMatchingRules: [
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'wauZK2QNEfDPwcAQo',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'ModalitiesInStudy',
|
||||||
|
constraint: {
|
||||||
|
contains: ['CT', 'PT'],
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'wauZK2QNEfDPwcAQo',
|
id: 'wauZK2QNEfDPwcAQo',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
@ -632,8 +641,8 @@ const ptCT = {
|
|||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'ptCT',
|
id: ptCT.id,
|
||||||
protocols: [ptCT],
|
protocol: ptCT,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
|
|||||||
const ohif = {
|
const ohif = {
|
||||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||||
hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracked = {
|
const tracked = {
|
||||||
@ -103,6 +102,7 @@ function modeFactory() {
|
|||||||
'Pan',
|
'Pan',
|
||||||
'Capture',
|
'Capture',
|
||||||
'Layout',
|
'Layout',
|
||||||
|
'MPR',
|
||||||
'MoreTools',
|
'MoreTools',
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
@ -169,7 +169,8 @@ function modeFactory() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensions: extensionDependencies,
|
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
|
// Order is important in sop class handlers when two handlers both use
|
||||||
// the same sop class under different situations. In that case, the more
|
// the same sop class under different situations. In that case, the more
|
||||||
// general handler needs to come last. For this case, the dicomvideo must
|
// general handler needs to come last. For this case, the dicomvideo must
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
function initDefaultToolGroup(
|
function initDefaultToolGroup(
|
||||||
extensionManager,
|
extensionManager,
|
||||||
ToolGroupService,
|
ToolGroupService,
|
||||||
commandsManager
|
commandsManager,
|
||||||
|
toolGroupId
|
||||||
) {
|
) {
|
||||||
const utilityModule = extensionManager.getModuleEntry(
|
const utilityModule = extensionManager.getModuleEntry(
|
||||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||||
@ -57,7 +58,6 @@ function initDefaultToolGroup(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolGroupId = 'default';
|
|
||||||
ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig);
|
ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,8 +140,19 @@ function initSRToolGroup(extensionManager, ToolGroupService, commandsManager) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initToolGroups(extensionManager, ToolGroupService, commandsManager) {
|
function initToolGroups(extensionManager, ToolGroupService, commandsManager) {
|
||||||
initDefaultToolGroup(extensionManager, ToolGroupService, commandsManager);
|
initDefaultToolGroup(
|
||||||
|
extensionManager,
|
||||||
|
ToolGroupService,
|
||||||
|
commandsManager,
|
||||||
|
'default'
|
||||||
|
);
|
||||||
initSRToolGroup(extensionManager, ToolGroupService, commandsManager);
|
initSRToolGroup(extensionManager, ToolGroupService, commandsManager);
|
||||||
|
initDefaultToolGroup(
|
||||||
|
extensionManager,
|
||||||
|
ToolGroupService,
|
||||||
|
commandsManager,
|
||||||
|
'mpr'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default initToolGroups;
|
export default initToolGroups;
|
||||||
|
|||||||
@ -290,6 +290,23 @@ const toolbarButtons = [
|
|||||||
columns: 3,
|
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...
|
// More...
|
||||||
{
|
{
|
||||||
id: 'MoreTools',
|
id: 'MoreTools',
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import setFusionActiveVolume from './utils/setFusionActiveVolume.js';
|
|||||||
const ohif = {
|
const ohif = {
|
||||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||||
hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default',
|
|
||||||
measurements: '@ohif/extension-default.panelModule.measure',
|
measurements: '@ohif/extension-default.panelModule.measure',
|
||||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||||
};
|
};
|
||||||
@ -188,7 +187,7 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
extensions: extensionDependencies,
|
extensions: extensionDependencies,
|
||||||
hangingProtocols: [tmtv.hangingProtocols],
|
hangingProtocol: 'ptCT',
|
||||||
sopClassHandlers: [ohif.sopClassHandler],
|
sopClassHandlers: [ohif.sopClassHandler],
|
||||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -43,12 +43,10 @@ export default class ExtensionManager {
|
|||||||
const {
|
const {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
|
||||||
} = _servicesManager.services;
|
} = _servicesManager.services;
|
||||||
|
|
||||||
MeasurementService.clearMeasurements();
|
MeasurementService.clearMeasurements();
|
||||||
ViewportGridService.reset();
|
ViewportGridService.reset();
|
||||||
HangingProtocolService.reset();
|
|
||||||
|
|
||||||
registeredExtensionIds.forEach(extensionId => {
|
registeredExtensionIds.forEach(extensionId => {
|
||||||
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
||||||
@ -74,12 +72,10 @@ export default class ExtensionManager {
|
|||||||
const {
|
const {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
|
||||||
} = _servicesManager.services;
|
} = _servicesManager.services;
|
||||||
|
|
||||||
MeasurementService.clearMeasurements();
|
MeasurementService.clearMeasurements();
|
||||||
ViewportGridService.reset();
|
ViewportGridService.reset();
|
||||||
HangingProtocolService.reset();
|
|
||||||
|
|
||||||
registeredExtensionIds.forEach(extensionId => {
|
registeredExtensionIds.forEach(extensionId => {
|
||||||
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
||||||
@ -196,6 +192,8 @@ export default class ExtensionManager {
|
|||||||
dataSources
|
dataSources
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||||
|
this._initHangingProtocolsModule(extensionModule, extensionId);
|
||||||
case MODULE_TYPES.TOOLBAR:
|
case MODULE_TYPES.TOOLBAR:
|
||||||
case MODULE_TYPES.VIEWPORT:
|
case MODULE_TYPES.VIEWPORT:
|
||||||
case MODULE_TYPES.PANEL:
|
case MODULE_TYPES.PANEL:
|
||||||
@ -203,7 +201,6 @@ export default class ExtensionManager {
|
|||||||
case MODULE_TYPES.CONTEXT:
|
case MODULE_TYPES.CONTEXT:
|
||||||
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
case MODULE_TYPES.LAYOUT_TEMPLATE:
|
||||||
case MODULE_TYPES.UTILITY:
|
case MODULE_TYPES.UTILITY:
|
||||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
|
||||||
// Default for most extension points,
|
// Default for most extension points,
|
||||||
// Just adds each entry ready for consumption by mode.
|
// Just adds each entry ready for consumption by mode.
|
||||||
extensionModule.forEach(element => {
|
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 = []) {
|
_initDataSourcesModule(extensionModule, extensionId, dataSources = []) {
|
||||||
const { UserAuthenticationService } = this._servicesManager.services;
|
const { UserAuthenticationService } = this._servicesManager.services;
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,9 @@ describe('ExtensionManager.js', () => {
|
|||||||
services: {
|
services: {
|
||||||
// Required for DataSource Module initiation
|
// Required for DataSource Module initiation
|
||||||
UserAuthenticationService: jest.fn(),
|
UserAuthenticationService: jest.fn(),
|
||||||
|
HangingProtocolService: {
|
||||||
|
addProtocol: jest.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
appConfig = {
|
appConfig = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ interface IDisplaySet {
|
|||||||
displaySetInstanceUID: string;
|
displaySetInstanceUID: string;
|
||||||
StudyInstanceUID: string;
|
StudyInstanceUID: string;
|
||||||
SeriesInstanceUID?: string;
|
SeriesInstanceUID?: string;
|
||||||
|
SeriesNumber?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IDisplaySet;
|
export default IDisplaySet;
|
||||||
|
|||||||
@ -127,7 +127,7 @@ describe('HangingProtocolService', () => {
|
|||||||
let initialScaling;
|
let initialScaling;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
hps.addProtocols([testProtocol]);
|
hps.addProtocol(testProtocol.id, testProtocol);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has one protocol', () => {
|
it('has one protocol', () => {
|
||||||
@ -137,11 +137,7 @@ describe('HangingProtocolService', () => {
|
|||||||
describe('run', () => {
|
describe('run', () => {
|
||||||
it('matches best image match', () => {
|
it('matches best image match', () => {
|
||||||
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
||||||
const {
|
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails();
|
||||||
hpAlreadyApplied,
|
|
||||||
viewportMatchDetails,
|
|
||||||
displaySetMatchDetails,
|
|
||||||
} = hps.getMatchDetails();
|
|
||||||
expect(hpAlreadyApplied).toMatchObject([false]);
|
expect(hpAlreadyApplied).toMatchObject([false]);
|
||||||
expect(viewportMatchDetails.length).toBe(1);
|
expect(viewportMatchDetails.length).toBe(1);
|
||||||
expect(viewportMatchDetails[0]).toMatchObject({
|
expect(viewportMatchDetails[0]).toMatchObject({
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import sortBy from '../../utils/sortBy';
|
|||||||
import ProtocolEngine from './ProtocolEngine';
|
import ProtocolEngine from './ProtocolEngine';
|
||||||
import StudyMetadata from '../../types/StudyMetadata';
|
import StudyMetadata from '../../types/StudyMetadata';
|
||||||
import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
||||||
|
import { HangingProtocol } from '../../types';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
||||||
@ -12,51 +13,24 @@ const EVENTS = {
|
|||||||
'event::hanging_protocol_custom_image_load_performed',
|
'event::hanging_protocol_custom_image_load_performed',
|
||||||
};
|
};
|
||||||
|
|
||||||
type ViewportOptions = {
|
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
|
||||||
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[];
|
|
||||||
};
|
|
||||||
|
|
||||||
class HangingProtocolService {
|
class HangingProtocolService {
|
||||||
studies: StudyMetadata[];
|
studies: StudyMetadata[];
|
||||||
protocols: Record<string, unknown>[];
|
// stores all the protocols (object or function that returns an object) in a map
|
||||||
protocol: Record<string, unknown>;
|
protocols: Map<string, Protocol>;
|
||||||
|
// the current protocol that is being applied to the viewports in object format
|
||||||
|
protocol: HangingProtocol.Protocol;
|
||||||
stage: number;
|
stage: number;
|
||||||
_commandsManager: Record<string, unknown>;
|
_commandsManager: Record<string, unknown>;
|
||||||
|
_servicesManager: Record<string, unknown>;
|
||||||
protocolEngine: ProtocolEngine;
|
protocolEngine: ProtocolEngine;
|
||||||
hpAlreadyApplied: boolean[] = [];
|
hpAlreadyApplied: boolean[] = [];
|
||||||
customViewportSettings = [];
|
customViewportSettings = [];
|
||||||
displaySets: IDisplaySet[] = [];
|
displaySets: IDisplaySet[] = [];
|
||||||
activeStudy: Record<string, unknown>;
|
activeStudy: Record<string, unknown>;
|
||||||
debugLogging: false;
|
debugLogging: false;
|
||||||
|
EVENTS: { [key: string]: string };
|
||||||
|
|
||||||
customAttributeRetrievalCallbacks = {
|
customAttributeRetrievalCallbacks = {
|
||||||
NumberOfStudyRelatedSeries: {
|
NumberOfStudyRelatedSeries: {
|
||||||
@ -89,17 +63,21 @@ class HangingProtocolService {
|
|||||||
* DisplaySetId is the id defined in the hangingProtocol object itself
|
* DisplaySetId is the id defined in the hangingProtocol object itself
|
||||||
* and match is an object that contains information about
|
* 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
|
* An array that contains for each viewport (viewportIndex) specified in the
|
||||||
* hanging protocol, an object of the form
|
* hanging protocol, an object of the form
|
||||||
*/
|
*/
|
||||||
viewportMatchDetails = [] as ViewportMatchDetails[];
|
viewportMatchDetails = [] as HangingProtocol.ViewportMatchDetails[];
|
||||||
|
|
||||||
constructor(commandsManager) {
|
constructor(commandsManager, servicesManager) {
|
||||||
this._commandsManager = commandsManager;
|
this._commandsManager = commandsManager;
|
||||||
this.protocols = [];
|
this._servicesManager = servicesManager;
|
||||||
|
this.protocols = new Map();
|
||||||
this.protocolEngine = undefined;
|
this.protocolEngine = undefined;
|
||||||
this.protocol = undefined;
|
this.protocol = undefined;
|
||||||
this.stage = undefined;
|
this.stage = undefined;
|
||||||
@ -116,13 +94,17 @@ class HangingProtocolService {
|
|||||||
|
|
||||||
public reset() {
|
public reset() {
|
||||||
this.studies = [];
|
this.studies = [];
|
||||||
this.protocols = [];
|
this.protocols = new Map();
|
||||||
this.hpAlreadyApplied = [];
|
this.hpAlreadyApplied = [];
|
||||||
this.viewportMatchDetails = [];
|
this.viewportMatchDetails = [];
|
||||||
// this.ProtocolEngine.reset()
|
// this.ProtocolEngine.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMatchDetails(): HangingProtocolMatchDetails {
|
public getDefaultProtocol(): HangingProtocol.Protocol {
|
||||||
|
return this.getProtocolById('default');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMatchDetails(): HangingProtocol.HangingProtocolMatchDetails {
|
||||||
return {
|
return {
|
||||||
viewportMatchDetails: this.viewportMatchDetails,
|
viewportMatchDetails: this.viewportMatchDetails,
|
||||||
displaySetMatchDetails: this.displaySetMatchDetails,
|
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 => {
|
* Returns the protocol with the given id, it will get the protocol from the
|
||||||
if (this.protocols.indexOf(protocol) === -1) {
|
* protocols map object and if it is a function, it will execute it and return
|
||||||
this.protocols.push(this._validateProtocol(protocol));
|
* 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.
|
* @param protocol is a specific protocol to apply.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public run({ studies, displaySets, activeStudy }, protocol) {
|
public run({ studies, displaySets, activeStudy }, protocolId) {
|
||||||
this.studies = [...studies];
|
this.studies = [...studies];
|
||||||
this.displaySets = displaySets;
|
this.displaySets = displaySets;
|
||||||
this.activeStudy = activeStudy || studies[0];
|
this.activeStudy = activeStudy || studies[0];
|
||||||
|
|
||||||
this.protocolEngine = new ProtocolEngine(
|
this.protocolEngine = new ProtocolEngine(
|
||||||
this.protocols,
|
this.getProtocols(),
|
||||||
this.customAttributeRetrievalCallbacks
|
this.customAttributeRetrievalCallbacks
|
||||||
);
|
);
|
||||||
|
|
||||||
// if there is no pre-defined protocol
|
if (protocolId) {
|
||||||
if (!protocol || protocol.id === undefined) {
|
const protocol = this.getProtocolById(protocolId);
|
||||||
const matchedProtocol = this.protocolEngine.run({
|
this._setProtocol(protocol);
|
||||||
studies: this.studies,
|
|
||||||
activeStudy,
|
|
||||||
displaySets,
|
|
||||||
});
|
|
||||||
this._setProtocol(matchedProtocol);
|
|
||||||
return;
|
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
|
* and its callback has been added to the HangingProtocolService
|
||||||
* @returns {boolean} true
|
* @returns {boolean} true
|
||||||
*/
|
*/
|
||||||
public hasCustomImageLoadStrategy() {
|
public hasCustomImageLoadStrategy(): boolean {
|
||||||
return (
|
return (
|
||||||
this.activeImageLoadStrategyName !== null &&
|
this.activeImageLoadStrategyName !== null &&
|
||||||
this.registeredImageLoadStrategies[
|
this.registeredImageLoadStrategies[
|
||||||
@ -194,7 +236,7 @@ class HangingProtocolService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCustomImageLoadPerformed() {
|
public getCustomImageLoadPerformed(): boolean {
|
||||||
return this.customImageLoadPerformed;
|
return this.customImageLoadPerformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,13 +245,13 @@ class HangingProtocolService {
|
|||||||
* @param {string} name strategy name
|
* @param {string} name strategy name
|
||||||
* @param {Function} callback image loader callback
|
* @param {Function} callback image loader callback
|
||||||
*/
|
*/
|
||||||
public registerImageLoadStrategy(name, callback) {
|
public registerImageLoadStrategy(name, callback): void {
|
||||||
if (callback instanceof Function && name) {
|
if (callback instanceof Function && name) {
|
||||||
this.registeredImageLoadStrategies[name] = callback;
|
this.registeredImageLoadStrategies[name] = callback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public setHangingProtocolAppliedForViewport(i) {
|
public setHangingProtocolAppliedForViewport(i): void {
|
||||||
this.hpAlreadyApplied[i] = true;
|
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 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
|
* @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] = {
|
this.customAttributeRetrievalCallbacks[attributeId] = {
|
||||||
...options,
|
...options,
|
||||||
id: attributeId,
|
id: attributeId,
|
||||||
@ -234,7 +281,7 @@ class HangingProtocolService {
|
|||||||
/**
|
/**
|
||||||
* Switches to the next protocol stage in the display set sequence
|
* Switches to the next protocol stage in the display set sequence
|
||||||
*/
|
*/
|
||||||
public nextProtocolStage() {
|
public nextProtocolStage(): void {
|
||||||
console.log('ProtocolEngine::nextProtocolStage');
|
console.log('ProtocolEngine::nextProtocolStage');
|
||||||
|
|
||||||
if (!this._setCurrentProtocolStage(1)) {
|
if (!this._setCurrentProtocolStage(1)) {
|
||||||
@ -245,7 +292,7 @@ class HangingProtocolService {
|
|||||||
/**
|
/**
|
||||||
* Switches to the previous protocol stage in the display set sequence
|
* Switches to the previous protocol stage in the display set sequence
|
||||||
*/
|
*/
|
||||||
public previousProtocolStage() {
|
public previousProtocolStage(): void {
|
||||||
console.log('ProtocolEngine::previousProtocolStage');
|
console.log('ProtocolEngine::previousProtocolStage');
|
||||||
|
|
||||||
if (!this._setCurrentProtocolStage(-1)) {
|
if (!this._setCurrentProtocolStage(-1)) {
|
||||||
@ -257,7 +304,7 @@ class HangingProtocolService {
|
|||||||
* Executes the callback function for the custom loading strategy for the images
|
* Executes the callback function for the custom loading strategy for the images
|
||||||
* if no strategy is set, the default strategy is used
|
* if no strategy is set, the default strategy is used
|
||||||
*/
|
*/
|
||||||
runImageLoadStrategy(data) {
|
runImageLoadStrategy(data): void {
|
||||||
const loader = this.registeredImageLoadStrategies[
|
const loader = this.registeredImageLoadStrategies[
|
||||||
this.activeImageLoadStrategyName
|
this.activeImageLoadStrategyName
|
||||||
];
|
];
|
||||||
@ -277,8 +324,14 @@ class HangingProtocolService {
|
|||||||
this._broadcastChange(this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, loadedData);
|
this._broadcastChange(this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, loadedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
_validateProtocol(protocol) {
|
_validateProtocol(
|
||||||
|
protocol: HangingProtocol.Protocol
|
||||||
|
): HangingProtocol.Protocol {
|
||||||
protocol.id = protocol.id || protocol.name;
|
protocol.id = protocol.id || protocol.name;
|
||||||
|
const defaultViewportOptions = {
|
||||||
|
toolGroupId: 'default',
|
||||||
|
viewportType: 'stack',
|
||||||
|
};
|
||||||
// Automatically compute some number of attributes if they
|
// Automatically compute some number of attributes if they
|
||||||
// aren't present. Makes defining new HPs easier.
|
// aren't present. Makes defining new HPs easier.
|
||||||
protocol.name = protocol.name || protocol.id;
|
protocol.name = protocol.name || protocol.id;
|
||||||
@ -292,13 +345,14 @@ class HangingProtocolService {
|
|||||||
|
|
||||||
for (let i = 0; i < rows * columns; i++) {
|
for (let i = 0; i < rows * columns; i++) {
|
||||||
stage.viewports.push({
|
stage.viewports.push({
|
||||||
viewportOptions: {},
|
viewportOptions: defaultViewportOptions,
|
||||||
displaySets: [],
|
displaySets: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stage.viewports.forEach(viewport => {
|
stage.viewports.forEach(viewport => {
|
||||||
viewport.viewportOptions = viewport.viewportOptions || {};
|
viewport.viewportOptions =
|
||||||
|
viewport.viewportOptions || defaultViewportOptions;
|
||||||
if (!viewport.displaySets) {
|
if (!viewport.displaySets) {
|
||||||
viewport.displaySets = [];
|
viewport.displaySets = [];
|
||||||
} else {
|
} else {
|
||||||
@ -313,9 +367,53 @@ class HangingProtocolService {
|
|||||||
return protocol;
|
return protocol;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setProtocol(protocol) {
|
/**
|
||||||
// TODO: Add proper Protocol class to validate the protocols
|
* It applied the protocol to the current studies and display sets based on the
|
||||||
// which are entered manually
|
* 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.stage = 0;
|
||||||
this.protocol = protocol;
|
this.protocol = protocol;
|
||||||
const { imageLoadStrategy } = protocol;
|
const { imageLoadStrategy } = protocol;
|
||||||
@ -328,7 +426,7 @@ class HangingProtocolService {
|
|||||||
this.activeImageLoadStrategyName = imageLoadStrategy;
|
this.activeImageLoadStrategyName = imageLoadStrategy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._updateViewports();
|
this._updateViewports(matchingDisplaySets);
|
||||||
|
|
||||||
this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, {
|
this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, {
|
||||||
viewportMatchDetails: this.viewportMatchDetails,
|
viewportMatchDetails: this.viewportMatchDetails,
|
||||||
@ -362,19 +460,50 @@ class HangingProtocolService {
|
|||||||
return this.protocol.stages[this.stage];
|
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.
|
* 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
|
// Make sure we have an active protocol with a non-empty array of display sets
|
||||||
if (!this._getNumProtocolStages()) {
|
if (!this._getNumProtocolStages()) {
|
||||||
console.log('No protocol stages - nothing to display');
|
console.log('No protocol stages - nothing to display');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// each time we are updating the viewports, we need to reset the
|
||||||
|
// matching applied
|
||||||
|
this.hpAlreadyApplied = [];
|
||||||
|
|
||||||
// reset displaySetMatchDetails
|
// reset displaySetMatchDetails
|
||||||
this.displaySetMatchDetails = new Map();
|
this.displaySetMatchDetails = new Map();
|
||||||
|
|
||||||
|
if (matchingDisplaySets) {
|
||||||
|
this.displaySetMatchDetails = new Map(
|
||||||
|
Object.entries(matchingDisplaySets)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the current stage
|
// Retrieve the current stage
|
||||||
const stageModel = this._getCurrentStageModel();
|
const stageModel = this._getCurrentStageModel();
|
||||||
|
|
||||||
@ -392,7 +521,7 @@ class HangingProtocolService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.customImageLoadPerformed = false;
|
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
|
// Retrieve the properties associated with the current display set's viewport structure template
|
||||||
// If no such layout properties exist, stop here.
|
// If no such layout properties exist, stop here.
|
||||||
@ -412,14 +541,19 @@ class HangingProtocolService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Matching the displaySets
|
// 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);
|
const { bestMatch, matchingScores } = this._matchImages(displaySet);
|
||||||
this.displaySetMatchDetails.set(displaySet.id, bestMatch);
|
this.displaySetMatchDetails.set(displaySet.id, bestMatch);
|
||||||
|
|
||||||
if (bestMatch) {
|
if (bestMatch) {
|
||||||
bestMatch.matchingScores = matchingScores;
|
bestMatch.matchingScores = matchingScores;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Loop through each viewport
|
// Loop through each viewport
|
||||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||||
@ -430,6 +564,8 @@ class HangingProtocolService {
|
|||||||
// but it is a info to locate the displaySet from the displaySetService
|
// but it is a info to locate the displaySet from the displaySetService
|
||||||
const displaySetsInfo = [];
|
const displaySetsInfo = [];
|
||||||
viewport.displaySets.forEach(
|
viewport.displaySets.forEach(
|
||||||
|
// Todo: why do we have displaySetIndex here? It is not used in the protocol
|
||||||
|
// definition
|
||||||
({ id, displaySetIndex = 0, options: displaySetOptions }) => {
|
({ id, displaySetIndex = 0, options: displaySetOptions }) => {
|
||||||
const viewportDisplaySetMain = this.displaySetMatchDetails.get(id);
|
const viewportDisplaySetMain = this.displaySetMatchDetails.get(id);
|
||||||
// Use the display set index to allow getting the "next" match, eg
|
// Use the display set index to allow getting the "next" match, eg
|
||||||
@ -445,7 +581,7 @@ class HangingProtocolService {
|
|||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
} = viewportDisplaySet;
|
} = viewportDisplaySet;
|
||||||
|
|
||||||
const displaySetInfo = {
|
const displaySetInfo: HangingProtocol.DisplaySetInfo = {
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
@ -524,6 +660,7 @@ class HangingProtocolService {
|
|||||||
const seriesMatchDetails = this.protocolEngine.findMatch(
|
const seriesMatchDetails = this.protocolEngine.findMatch(
|
||||||
displaySet,
|
displaySet,
|
||||||
seriesMatchingRules,
|
seriesMatchingRules,
|
||||||
|
// Todo: why we have images here since the matching type does not have it
|
||||||
{ studies: this.studies, instance: displaySet.images?.[0] }
|
{ studies: this.studies, instance: displaySet.images?.[0] }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import HangingProtocolService from './HangingProtocolService';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HangingProtocolService',
|
name: 'HangingProtocolService',
|
||||||
create: ({ configuration = {}, commandsManager }) => {
|
create: ({ configuration = {}, commandsManager, servicesManager }) => {
|
||||||
return new HangingProtocolService(commandsManager);
|
return new HangingProtocolService(commandsManager, servicesManager);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export default class ServicesManager {
|
|||||||
this.services[service.name] = service.create({
|
this.services[service.name] = service.create({
|
||||||
configuration,
|
configuration,
|
||||||
commandsManager: this._commandsManager,
|
commandsManager: this._commandsManager,
|
||||||
|
servicesManager: this,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.warn(`Service create factory function not defined. Exiting early.`);
|
log.warn(`Service create factory function not defined. Exiting early.`);
|
||||||
|
|||||||
@ -4,10 +4,15 @@ import log from '../log.js';
|
|||||||
jest.mock('./../log.js');
|
jest.mock('./../log.js');
|
||||||
|
|
||||||
describe('ServicesManager.js', () => {
|
describe('ServicesManager.js', () => {
|
||||||
let servicesManager;
|
let servicesManager, commandsManager;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
servicesManager = new ServicesManager();
|
commandsManager = {
|
||||||
|
createContext: jest.fn(),
|
||||||
|
getContext: jest.fn(),
|
||||||
|
registerCommand: jest.fn(),
|
||||||
|
};
|
||||||
|
servicesManager = new ServicesManager(commandsManager);
|
||||||
log.warn.mockClear();
|
log.warn.mockClear();
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@ -90,9 +95,9 @@ describe('ServicesManager.js', () => {
|
|||||||
|
|
||||||
servicesManager.registerService(fakeService, configuration);
|
servicesManager.registerService(fakeService, configuration);
|
||||||
|
|
||||||
expect(fakeService.create.mock.calls[0][0]).toEqual({
|
expect(fakeService.create.mock.calls[0][0].configuration.config).toBe(
|
||||||
configuration,
|
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';
|
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',
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
name: 'default',
|
name: 'default',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
type: 'grid',
|
layoutType: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
rows: 1,
|
rows: 1,
|
||||||
columns: 2,
|
columns: 2,
|
||||||
@ -64,8 +64,8 @@ const deafultProtocol = {
|
|||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: hangingProtocolName,
|
id: 'default',
|
||||||
protocols: [defaultProtocol],
|
protocol: deafultProtocol,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,8 +149,8 @@ async function defaultRouteInit({
|
|||||||
});
|
});
|
||||||
|
|
||||||
DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {
|
DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {
|
||||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
const studyMetadata = // get study metadata and displaySets
|
||||||
HangingProtocolService.run(studyMetadata);
|
HangingProtocolService.run({studies, displaySets, activeStudy});
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscriptions;
|
return unsubscriptions;
|
||||||
|
|||||||
@ -8,20 +8,32 @@ sidebar_label: Hanging Protocol Service
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`HangingProtocolService` is a migration of the `OHIF-v1` hanging protocol
|
`HangingProtocolService` is a migration of the `OHIF-v1` hanging protocol
|
||||||
engine. This service handles the arrangement of the images in the viewport. In
|
engine along with various improvements and fixes.
|
||||||
short, the registered protocols will get matched with the Series that are
|
This service handles the arrangement of the images in the viewport. In
|
||||||
available for the series. Each protocol gets a point, and they are ranked. The
|
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.
|
winning protocol gets applied and its settings run for the viewports.
|
||||||
|
|
||||||
You can read more about hanging protocols
|
You can read more about hanging protocols
|
||||||
[here](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf).
|
[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 what layout of the viewport should the viewer starts with (2x2 layout)
|
||||||
- Define which series gets displayed in which position of the layout
|
- Specify the type of the viewport and its orientation
|
||||||
- Apply certain initial viewport settings; e.g., inverting the contrast
|
- Define which displaySets gets displayed in which viewport of the layout
|
||||||
- Enable certain tools based on what series are displayed: link prostate T2 and
|
- Apply certain initial viewport settings; e.g., inverting the contrast, jumping to a specific slice, etc.
|
||||||
ADC MRI.
|
- 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
|
## 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` |
|
| 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 |
|
| 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
|
## 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
|
- `addProtocol`: adds provided protocol to the list of registered protocols
|
||||||
- `hpAlreadyApplied`: An array which tracks whether HPServices has been
|
|
||||||
applied on each viewport.
|
|
||||||
|
|
||||||
- `addProtocols`: adds provided protocols to the list of registered protocols
|
|
||||||
for matching
|
for matching
|
||||||
|
|
||||||
- `run(studyMetaData, protocol)`: runs the HPService with the provided
|
- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided
|
||||||
studyMetaData and optional protocol. If protocol is not given, HP Matching
|
studyMetaData and optional protocolId. If protocol is not given, HP Matching
|
||||||
engine will search all the registered protocols for the best matching one
|
engine will search all the registered protocols for the best matching one
|
||||||
based on the constraints.
|
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`
|
Default initialization of the modes handles running the `HangingProtocolService`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Custom Attribute
|
## Custom Attribute
|
||||||
In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance,
|
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.
|
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(
|
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
||||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||||
({ StudyInstanceUID }) => {
|
({ StudyInstanceUID }) => {
|
||||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
HangingProtocolService.run({studies, displaySets, activeStudy});
|
||||||
HangingProtocolService.run(studyMetadata);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
unsubscriptions.push(seriesAddedUnsubscribe);
|
unsubscriptions.push(seriesAddedUnsubscribe);
|
||||||
@ -89,11 +88,6 @@ export default function ModeRoute(/**..**/) {
|
|||||||
extensionManager.onModeEnter();
|
extensionManager.onModeEnter();
|
||||||
mode?.onModeEnter({ servicesManager, extensionManager });
|
mode?.onModeEnter({ servicesManager, extensionManager });
|
||||||
|
|
||||||
hangingProtocols.forEach(extentionProtocols => {
|
|
||||||
const { protocols } = extensionManager.getModuleEntry(extentionProtocols);
|
|
||||||
HangingProtocolService.addProtocols(protocols);
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupRouteInit = async () => {
|
const setupRouteInit = async () => {
|
||||||
if (route.init) {
|
if (route.init) {
|
||||||
return await route.init(/**...**/);
|
return await route.init(/**...**/);
|
||||||
|
|||||||
@ -49,11 +49,18 @@ export function ViewportGridProvider({ children, service }) {
|
|||||||
case 'SET_DISPLAYSET_FOR_VIEWPORT': {
|
case 'SET_DISPLAYSET_FOR_VIEWPORT': {
|
||||||
const payload = action.payload;
|
const payload = action.payload;
|
||||||
const { viewportIndex, displaySetInstanceUIDs } = payload;
|
const { viewportIndex, displaySetInstanceUIDs } = payload;
|
||||||
const viewport = state.viewports[viewportIndex];
|
|
||||||
const viewportOptions =
|
// Note: there should be no inheritance happening at this level,
|
||||||
payload.viewportOptions || viewport.viewportOptions || {};
|
// we can't assume the new displaySet can inherit the previous
|
||||||
const displaySetOptions = payload.displaySetOptions ||
|
// displaySet's or viewportOptions at all. For instance, dragging
|
||||||
viewport.displaySetOptions || [{}];
|
// 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();
|
const viewports = state.viewports.slice();
|
||||||
|
|
||||||
// merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
|
// merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
|
||||||
|
|||||||
@ -17,7 +17,10 @@ import Compose from './Compose';
|
|||||||
* @param props.dataSource to read the data from
|
* @param props.dataSource to read the data from
|
||||||
* @returns array of subscriptions to cancel
|
* @returns array of subscriptions to cancel
|
||||||
*/
|
*/
|
||||||
function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) {
|
function defaultRouteInit(
|
||||||
|
{ servicesManager, studyInstanceUIDs, dataSource },
|
||||||
|
hangingProtocol
|
||||||
|
) {
|
||||||
const {
|
const {
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
@ -75,8 +78,12 @@ function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) {
|
|||||||
// study being displayed, and is thus the "active" study.
|
// study being displayed, and is thus the "active" study.
|
||||||
const activeStudy = studies[0];
|
const activeStudy = studies[0];
|
||||||
|
|
||||||
// run the hanging protocol matching service on the displaySets
|
// run the hanging protocol matching on the displaySets with the predefined
|
||||||
HangingProtocolService.run({ studies, activeStudy, displaySets });
|
// hanging protocol in the mode configuration
|
||||||
|
HangingProtocolService.run(
|
||||||
|
{ studies, activeStudy, displaySets },
|
||||||
|
hangingProtocol
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscriptions;
|
return unsubscriptions;
|
||||||
@ -107,12 +114,9 @@ export default function ModeRoute({
|
|||||||
locationRef.current = location;
|
locationRef.current = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { DisplaySetService } = servicesManager.services;
|
||||||
DisplaySetService,
|
|
||||||
HangingProtocolService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
|
|
||||||
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
|
const { extensions, sopClassHandlers, hotkeys, hangingProtocol } = mode;
|
||||||
|
|
||||||
if (dataSourceName === undefined) {
|
if (dataSourceName === undefined) {
|
||||||
dataSourceName = extensionManager.defaultDataSourceName;
|
dataSourceName = extensionManager.defaultDataSourceName;
|
||||||
@ -239,34 +243,28 @@ export default function ModeRoute({
|
|||||||
});
|
});
|
||||||
mode?.onModeEnter({ servicesManager, extensionManager, commandsManager });
|
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 () => {
|
const setupRouteInit = async () => {
|
||||||
if (route.init) {
|
if (route.init) {
|
||||||
return await route.init({
|
return await route.init(
|
||||||
servicesManager,
|
{
|
||||||
extensionManager,
|
servicesManager,
|
||||||
hotkeysManager,
|
extensionManager,
|
||||||
studyInstanceUIDs,
|
hotkeysManager,
|
||||||
dataSource,
|
studyInstanceUIDs,
|
||||||
});
|
dataSource,
|
||||||
|
},
|
||||||
|
hangingProtocol
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultRouteInit({
|
return defaultRouteInit(
|
||||||
servicesManager,
|
{
|
||||||
studyInstanceUIDs,
|
servicesManager,
|
||||||
dataSource,
|
studyInstanceUIDs,
|
||||||
});
|
dataSource,
|
||||||
|
},
|
||||||
|
hangingProtocol
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let unsubscriptions;
|
let unsubscriptions;
|
||||||
@ -291,7 +289,6 @@ export default function ModeRoute({
|
|||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
studyInstanceUIDs,
|
studyInstanceUIDs,
|
||||||
refresh,
|
refresh,
|
||||||
hangingProtocols,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const renderLayoutData = props => {
|
const renderLayoutData = props => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user