feat: make hanging protocol work on displaySets instead of series (#2837)
* feat(HP): Apply HP to display sets, fix race condition * fix: displaySetService no event not needed (#2912) * fix various styles and renamings * feat: refactored hp service * fix: use HP service event for viewport grid * remove unnecessary doc * fix test * apply review comment * fix: segmentation creation Co-authored-by: Alireza <ar.sedghi@gmail.com>
This commit is contained in:
parent
7c0756824e
commit
e1d366e1e1
@ -39,6 +39,8 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
renderingEngine: Types.IRenderingEngine | null;
|
renderingEngine: Types.IRenderingEngine | null;
|
||||||
viewportsInfo: Map<number, ViewportInfo>;
|
viewportsInfo: Map<number, ViewportInfo>;
|
||||||
viewportGridResizeObserver: ResizeObserver | null;
|
viewportGridResizeObserver: ResizeObserver | null;
|
||||||
|
// TODO - get the right type here.
|
||||||
|
hangingProtocolService: object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service-specific
|
* Service-specific
|
||||||
@ -59,7 +61,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
const { HangingProtocolService } = servicesManager.services;
|
const { HangingProtocolService } = servicesManager.services;
|
||||||
this.HangingProtocolService = HangingProtocolService;
|
this.hangingProtocolService = HangingProtocolService;
|
||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -166,8 +168,10 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||||
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
||||||
|
|
||||||
const { viewportOptions, displaySetOptions } =
|
const {
|
||||||
this._getViewportAndDisplaySetOptions(
|
viewportOptions,
|
||||||
|
displaySetOptions,
|
||||||
|
} = this._getViewportAndDisplaySetOptions(
|
||||||
publicViewportOptions,
|
publicViewportOptions,
|
||||||
publicDisplaySetOptions,
|
publicDisplaySetOptions,
|
||||||
viewportInfo
|
viewportInfo
|
||||||
@ -265,7 +269,6 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo
|
||||||
) {
|
) {
|
||||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
const { imageIds, initialImageIndex } = viewportData;
|
const { imageIds, initialImageIndex } = viewportData;
|
||||||
|
|
||||||
let initialImageIndexToUse = initialImageIndex;
|
let initialImageIndexToUse = initialImageIndex;
|
||||||
@ -352,7 +355,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
// (This call may or may not create sub-requests for series metadata)
|
// (This call may or may not create sub-requests for series metadata)
|
||||||
const volumeInputArray = [];
|
const volumeInputArray = [];
|
||||||
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
||||||
const { HangingProtocolService } = this;
|
const { hangingProtocolService } = this;
|
||||||
|
|
||||||
for (let i = 0; i < viewportData.imageIds.length; i++) {
|
for (let i = 0; i < viewportData.imageIds.length; i++) {
|
||||||
const imageIds = viewportData.imageIds[i];
|
const imageIds = viewportData.imageIds[i];
|
||||||
@ -384,11 +387,11 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
HangingProtocolService.hasCustomImageLoadStrategy() &&
|
hangingProtocolService.hasCustomImageLoadStrategy() &&
|
||||||
!HangingProtocolService.customImageLoadPerformed
|
!hangingProtocolService.customImageLoadPerformed
|
||||||
) {
|
) {
|
||||||
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
||||||
return HangingProtocolService.runImageLoadStrategy({
|
return hangingProtocolService.runImageLoadStrategy({
|
||||||
viewportId: viewport.id,
|
viewportId: viewport.id,
|
||||||
volumeInputArray,
|
volumeInputArray,
|
||||||
});
|
});
|
||||||
@ -415,8 +418,9 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
) {
|
) {
|
||||||
const { index, preset } = initialImageOptions;
|
const { index, preset } = initialImageOptions;
|
||||||
|
|
||||||
const { numberOfSlices } =
|
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
|
||||||
csUtils.getImageSliceDataForVolumeViewport(viewport);
|
viewport
|
||||||
|
);
|
||||||
|
|
||||||
const imageIndex = this._getInitialImageIndex(
|
const imageIndex = this._getInitialImageIndex(
|
||||||
numberOfSlices,
|
numberOfSlices,
|
||||||
|
|||||||
@ -16,7 +16,6 @@ export type ViewportOptions = {
|
|||||||
viewportId: string;
|
viewportId: string;
|
||||||
orientation?: Types.Orientation;
|
orientation?: Types.Orientation;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
initialView?: string;
|
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: SyncGroup[];
|
||||||
initialImageOptions?: InitialImageOptions;
|
initialImageOptions?: InitialImageOptions;
|
||||||
customViewportProps?: Record<string, unknown>;
|
customViewportProps?: Record<string, unknown>;
|
||||||
@ -28,7 +27,6 @@ export type PublicViewportOptions = {
|
|||||||
viewportId?: string;
|
viewportId?: string;
|
||||||
orientation?: string;
|
orientation?: string;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
initialView?: string;
|
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: SyncGroup[];
|
||||||
initialImageOptions?: InitialImageOptions;
|
initialImageOptions?: InitialImageOptions;
|
||||||
customViewportProps?: Record<string, unknown>;
|
customViewportProps?: Record<string, unknown>;
|
||||||
|
|||||||
@ -8,8 +8,8 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This function caches the volumeUIDs until all the volumes inside the
|
* This function caches the volumeUIDs until all the volumes inside the
|
||||||
* hangging protocol are initialized. Then it goes through the imageIds
|
* hanging protocol are initialized. Then it goes through the imageIds
|
||||||
* of the volumes, and interleav them, in order for the volumes to be loaded
|
* of the volumes, and interleave them, in order for the volumes to be loaded
|
||||||
* together from middle to the start and the end.
|
* together from middle to the start and the end.
|
||||||
* @param {Object} props image loading properties from Cornerstone ViewportService
|
* @param {Object} props image loading properties from Cornerstone ViewportService
|
||||||
* @returns
|
* @returns
|
||||||
@ -17,7 +17,7 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
export default function interleaveCenterLoader({
|
export default function interleaveCenterLoader({
|
||||||
data: { viewportId, volumeInputArray },
|
data: { viewportId, volumeInputArray },
|
||||||
displaySetsMatchDetails,
|
displaySetsMatchDetails,
|
||||||
matchDetails,
|
viewportMatchDetails: matchDetails,
|
||||||
}) {
|
}) {
|
||||||
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
export default function interleaveTopToBottom({
|
export default function interleaveTopToBottom({
|
||||||
data: { viewportId, volumeInputArray },
|
data: { viewportId, volumeInputArray },
|
||||||
displaySetsMatchDetails,
|
displaySetsMatchDetails,
|
||||||
matchDetails,
|
viewportMatchDetails: matchDetails,
|
||||||
}) {
|
}) {
|
||||||
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
|
|||||||
@ -473,12 +473,13 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
|
|
||||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||||
|
|
||||||
const numberOfSeries = seriesPromises.length;
|
const seriesDeliveredPromises = seriesPromises.map(promise =>
|
||||||
seriesPromises.forEach(async (seriesPromise, index) => {
|
promise.then(instances => {
|
||||||
const instances = await seriesPromise;
|
|
||||||
storeInstances(instances);
|
storeInstances(instances);
|
||||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
})
|
||||||
});
|
);
|
||||||
|
await Promise.all(seriesDeliveredPromises);
|
||||||
|
setSuccessFlag();
|
||||||
},
|
},
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet(displaySet) {
|
getImageIdsForDisplaySet(displaySet) {
|
||||||
|
|||||||
@ -23,7 +23,9 @@ const defaultProtocol = {
|
|||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
id: 'displaySet',
|
id: 'displaySet',
|
||||||
|
// Unused currently
|
||||||
imageMatchingRules: [],
|
imageMatchingRules: [],
|
||||||
|
// Matches displaysets, NOT series
|
||||||
seriesMatchingRules: [],
|
seriesMatchingRules: [],
|
||||||
studyMatchingRules: [],
|
studyMatchingRules: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -28,12 +28,12 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
ToolGroupService,
|
ToolGroupService,
|
||||||
ToolBarService,
|
ToolBarService,
|
||||||
|
HangingProtocolService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
const [metadata, setMetadata] = useState(DEFAULT_MEATADATA);
|
const [metadata, setMetadata] = useState(DEFAULT_MEATADATA);
|
||||||
const [ptDisplaySet, setPtDisplaySet] = useState(null);
|
const [ptDisplaySet, setPtDisplaySet] = useState(null);
|
||||||
|
|
||||||
const handleMetadataChange = useCallback(
|
const handleMetadataChange = metadata => {
|
||||||
metadata => {
|
|
||||||
setMetadata(prevState => {
|
setMetadata(prevState => {
|
||||||
const newState = { ...prevState };
|
const newState = { ...prevState };
|
||||||
Object.keys(metadata).forEach(key => {
|
Object.keys(metadata).forEach(key => {
|
||||||
@ -48,12 +48,12 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
});
|
});
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
[metadata]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMatchingPTDisplaySet = useCallback(() => {
|
const getMatchingPTDisplaySet = viewportMatchDetails => {
|
||||||
const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet');
|
const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet', {
|
||||||
|
viewportMatchDetails,
|
||||||
|
});
|
||||||
|
|
||||||
if (!ptDisplaySet) {
|
if (!ptDisplaySet) {
|
||||||
return;
|
return;
|
||||||
@ -67,16 +67,16 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
ptDisplaySet,
|
ptDisplaySet,
|
||||||
metadata,
|
metadata,
|
||||||
};
|
};
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const displaySets = DisplaySetService.activeDisplaySets;
|
const displaySets = DisplaySetService.getActiveDisplaySets();
|
||||||
|
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
|
||||||
if (!displaySets.length) {
|
if (!displaySets.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySetInfo = getMatchingPTDisplaySet();
|
const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails);
|
||||||
|
|
||||||
if (!displaySetInfo) {
|
if (!displaySetInfo) {
|
||||||
return;
|
return;
|
||||||
@ -89,15 +89,14 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
|||||||
|
|
||||||
// get the patientMetadata from the StudyInstanceUIDs and update the state
|
// get the patientMetadata from the StudyInstanceUIDs and update the state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = DisplaySetService.subscribe(
|
const { unsubscribe } = HangingProtocolService.subscribe(
|
||||||
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||||
() => {
|
({ viewportMatchDetails }) => {
|
||||||
const displaySetInfo = getMatchingPTDisplaySet();
|
const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails);
|
||||||
|
|
||||||
if (!displaySetInfo) {
|
if (!displaySetInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ptDisplaySet, metadata } = displaySetInfo;
|
const { ptDisplaySet, metadata } = displaySetInfo;
|
||||||
setPtDisplaySet(ptDisplaySet);
|
setPtDisplaySet(ptDisplaySet);
|
||||||
setMetadata(metadata);
|
setMetadata(metadata);
|
||||||
|
|||||||
@ -41,9 +41,9 @@ const commandsModule = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _getMatchedViewportsToolGroupIds() {
|
function _getMatchedViewportsToolGroupIds() {
|
||||||
const [matchedViewports] = HangingProtocolService.getState();
|
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
|
||||||
const toolGroupIds = [];
|
const toolGroupIds = [];
|
||||||
matchedViewports.forEach(({ viewportOptions }) => {
|
viewportMatchDetails.forEach(({ viewportOptions }) => {
|
||||||
const { toolGroupId } = viewportOptions;
|
const { toolGroupId } = viewportOptions;
|
||||||
if (toolGroupIds.indexOf(toolGroupId) === -1) {
|
if (toolGroupIds.indexOf(toolGroupId) === -1) {
|
||||||
toolGroupIds.push(toolGroupId);
|
toolGroupIds.push(toolGroupId);
|
||||||
@ -54,33 +54,30 @@ const commandsModule = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getMatchingPTDisplaySet: () => {
|
getMatchingPTDisplaySet: ({ viewportMatchDetails }) => {
|
||||||
// Todo: this is assuming that the hanging protocol has successfully matched
|
// Todo: this is assuming that the hanging protocol has successfully matched
|
||||||
// the correct PT. For future, we should have a way to filter out the PTs
|
// the correct PT. For future, we should have a way to filter out the PTs
|
||||||
// that are in the viewer layout (but then we have the problem of the attenuation
|
// that are in the viewer layout (but then we have the problem of the attenuation
|
||||||
// corrected PT vs the non-attenuation correct PT)
|
// corrected PT vs the non-attenuation correct PT)
|
||||||
const matches = HangingProtocolService.getDisplaySetsMatchDetails();
|
|
||||||
|
|
||||||
const matchedSeriesInstanceUIDs = Array.from(matches.values()).map(
|
|
||||||
({ SeriesInstanceUID }) => SeriesInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
let ptDisplaySet = null;
|
let ptDisplaySet = null;
|
||||||
for (const SeriesInstanceUID of matchedSeriesInstanceUIDs) {
|
for (const matched of viewportMatchDetails) {
|
||||||
const displaySets = DisplaySetService.getDisplaySetsForSeries(
|
const { displaySetsInfo } = matched;
|
||||||
SeriesInstanceUID
|
const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) =>
|
||||||
|
DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!displaySets || displaySets.length === 0) {
|
if (!displaySets || displaySets.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = displaySets[0];
|
ptDisplaySet = displaySets.find(
|
||||||
if (displaySet.Modality !== 'PT') {
|
displaySet => displaySet.Modality === 'PT'
|
||||||
continue;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
ptDisplaySet = displaySet;
|
if (ptDisplaySet) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ptDisplaySet;
|
return ptDisplaySet;
|
||||||
@ -121,7 +118,10 @@ const commandsModule = ({
|
|||||||
createNewLabelmapFromPT: async () => {
|
createNewLabelmapFromPT: async () => {
|
||||||
// Create a segmentation of the same resolution as the source data
|
// Create a segmentation of the same resolution as the source data
|
||||||
// using volumeLoader.createAndCacheDerivedVolume.
|
// using volumeLoader.createAndCacheDerivedVolume.
|
||||||
const ptDisplaySet = actions.getMatchingPTDisplaySet();
|
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
|
||||||
|
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
||||||
|
viewportMatchDetails,
|
||||||
|
});
|
||||||
|
|
||||||
if (!ptDisplaySet) {
|
if (!ptDisplaySet) {
|
||||||
UINotificationService.error('No matching PT display set found');
|
UINotificationService.error('No matching PT display set found');
|
||||||
@ -565,8 +565,11 @@ const commandsModule = ({
|
|||||||
},
|
},
|
||||||
setFusionPTColormap: ({ toolGroupId, colormap }) => {
|
setFusionPTColormap: ({ toolGroupId, colormap }) => {
|
||||||
const toolGroup = ToolGroupService.getToolGroup(toolGroupId);
|
const toolGroup = ToolGroupService.getToolGroup(toolGroupId);
|
||||||
|
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
|
||||||
|
|
||||||
const ptDisplaySet = actions.getMatchingPTDisplaySet();
|
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
||||||
|
viewportMatchDetails,
|
||||||
|
});
|
||||||
|
|
||||||
if (!ptDisplaySet) {
|
if (!ptDisplaySet) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -6,10 +6,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
verbose: true,
|
verbose: true,
|
||||||
roots: ['<rootDir>/src'],
|
roots: ['<rootDir>/src'],
|
||||||
transform: {
|
|
||||||
'^.+\\.js$': 'babel-jest',
|
|
||||||
'^.+\\.jsx$': 'babel-jest',
|
|
||||||
},
|
|
||||||
testMatch: ['<rootDir>/src/**/*.test.js'],
|
testMatch: ['<rootDir>/src/**/*.test.js'],
|
||||||
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
|
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
|
||||||
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
|
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],
|
||||||
|
|||||||
@ -100,17 +100,19 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
// For fusion toolGroup we need to add the volumeIds for the crosshairs
|
// For fusion toolGroup we need to add the volumeIds for the crosshairs
|
||||||
// since in the fusion viewport we don't want both PT and CT to render MIP
|
// since in the fusion viewport we don't want both PT and CT to render MIP
|
||||||
// when slabThickness is modified
|
// when slabThickness is modified
|
||||||
const matches = HangingProtocolService.getDisplaySetsMatchDetails();
|
const {
|
||||||
|
displaySetMatchDetails,
|
||||||
|
} = HangingProtocolService.getMatchDetails();
|
||||||
|
|
||||||
setCrosshairsConfiguration(
|
setCrosshairsConfiguration(
|
||||||
matches,
|
displaySetMatchDetails,
|
||||||
toolNames,
|
toolNames,
|
||||||
ToolGroupService,
|
ToolGroupService,
|
||||||
DisplaySetService
|
DisplaySetService
|
||||||
);
|
);
|
||||||
|
|
||||||
setFusionActiveVolume(
|
setFusionActiveVolume(
|
||||||
matches,
|
displaySetMatchDetails,
|
||||||
toolNames,
|
toolNames,
|
||||||
ToolGroupService,
|
ToolGroupService,
|
||||||
DisplaySetService
|
DisplaySetService
|
||||||
|
|||||||
@ -211,7 +211,14 @@ const BaseImplementation = {
|
|||||||
let study = _getStudy(StudyInstanceUID);
|
let study = _getStudy(StudyInstanceUID);
|
||||||
if (!study) {
|
if (!study) {
|
||||||
study = createStudyMetadata(StudyInstanceUID);
|
study = createStudyMetadata(StudyInstanceUID);
|
||||||
|
// Will typically be undefined with a compliant DICOMweb server, reset later
|
||||||
study.StudyDescription = seriesSummaryMetadata[0].StudyDescription;
|
study.StudyDescription = seriesSummaryMetadata[0].StudyDescription;
|
||||||
|
seriesSummaryMetadata.forEach(item => {
|
||||||
|
if (study.ModalitiesInStudy.indexOf(item.Modality) === -1) {
|
||||||
|
study.ModalitiesInStudy.push(item.Modality);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
study.NumberOfStudyRelatedSeries = seriesSummaryMetadata.length;
|
||||||
_model.studies.push(study);
|
_model.studies.push(study);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
/** Defines a typescript type for study metadata */
|
||||||
|
interface StudyMetadata {
|
||||||
|
readonly StudyInstanceUID: string;
|
||||||
|
StudyDescription?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StudyMetadata;
|
||||||
@ -4,6 +4,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
|||||||
return {
|
return {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
StudyDescription: '',
|
StudyDescription: '',
|
||||||
|
ModalitiesInStudy: [],
|
||||||
isLoaded: false,
|
isLoaded: false,
|
||||||
series: [],
|
series: [],
|
||||||
/**
|
/**
|
||||||
@ -13,6 +14,9 @@ function createStudyMetadata(StudyInstanceUID) {
|
|||||||
*/
|
*/
|
||||||
addInstanceToSeries: function(instance) {
|
addInstanceToSeries: function(instance) {
|
||||||
const { SeriesInstanceUID } = instance;
|
const { SeriesInstanceUID } = instance;
|
||||||
|
if (!this.StudyDescription) {
|
||||||
|
this.StudyDescription = instance.StudyDescription;
|
||||||
|
}
|
||||||
const existingSeries = this.series.find(
|
const existingSeries = this.series.find(
|
||||||
s => s.SeriesInstanceUID === SeriesInstanceUID
|
s => s.SeriesInstanceUID === SeriesInstanceUID
|
||||||
);
|
);
|
||||||
@ -22,16 +26,24 @@ function createStudyMetadata(StudyInstanceUID) {
|
|||||||
} else {
|
} else {
|
||||||
const series = createSeriesMetadata([instance]);
|
const series = createSeriesMetadata([instance]);
|
||||||
this.series.push(series);
|
this.series.push(series);
|
||||||
|
const { Modality } = series;
|
||||||
|
if (this.ModalitiesInStudy.indexof(Modality) === -1) {
|
||||||
|
this.ModalitiesInStudy.push(Modality);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {object[]} instances
|
* @param {object[]} instances
|
||||||
* @param {string} instances[].SeriesInstanceUID
|
* @param {string} instances[].SeriesInstanceUID
|
||||||
|
* @param {string} instances[].StudyDescription
|
||||||
* @returns {bool} true if series were added; false if series already exist
|
* @returns {bool} true if series were added; false if series already exist
|
||||||
*/
|
*/
|
||||||
addInstancesToSeries: function(instances) {
|
addInstancesToSeries: function(instances) {
|
||||||
const { SeriesInstanceUID } = instances[0];
|
const { SeriesInstanceUID } = instances[0];
|
||||||
|
if (!this.StudyDescription) {
|
||||||
|
this.StudyDescription = instances[0].StudyDescription;
|
||||||
|
}
|
||||||
const existingSeries = this.series.find(
|
const existingSeries = this.series.find(
|
||||||
s => s.SeriesInstanceUID === SeriesInstanceUID
|
s => s.SeriesInstanceUID === SeriesInstanceUID
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
interface IDisplaySet {
|
||||||
|
displaySetInstanceUID: string;
|
||||||
|
StudyInstanceUID: string;
|
||||||
|
SeriesInstanceUID?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IDisplaySet;
|
||||||
@ -4,10 +4,18 @@ import validate from './lib/validator';
|
|||||||
* Match a Metadata instance against rules using Validate.js for validation.
|
* Match a Metadata instance against rules using Validate.js for validation.
|
||||||
* @param {InstanceMetadata} metadataInstance Metadata instance object
|
* @param {InstanceMetadata} metadataInstance Metadata instance object
|
||||||
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
|
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
|
||||||
|
* @param {object} options is an object containing additional information
|
||||||
|
* @param {object[]} options.studies is a list of all the studies
|
||||||
|
* @param {object[]} options.displaySets is a list of the display sets
|
||||||
* @return {Object} Matching Object with score and details (which rule passed or failed)
|
* @return {Object} Matching Object with score and details (which rule passed or failed)
|
||||||
*/
|
*/
|
||||||
const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
const match = (
|
||||||
const options = {
|
metadataInstance,
|
||||||
|
rules,
|
||||||
|
customAttributeRetrievalCallbacks,
|
||||||
|
options
|
||||||
|
) => {
|
||||||
|
const validateOptions = {
|
||||||
format: 'grouped',
|
format: 'grouped',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,18 +24,32 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
|||||||
failed: [],
|
failed: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readValues = {};
|
||||||
|
|
||||||
let requiredFailed = false;
|
let requiredFailed = false;
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
rules.forEach(rule => {
|
rules.forEach(rule => {
|
||||||
const attribute = rule.attribute;
|
const { attribute } = rule;
|
||||||
|
|
||||||
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
||||||
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||||
const customAttribute = customAttributeRetrievalCallbacks[attribute];
|
readValues[attribute] = customAttributeRetrievalCallbacks[
|
||||||
metadataInstance[attribute] = customAttribute.callback(metadataInstance);
|
attribute
|
||||||
|
].callback(metadataInstance, options);
|
||||||
|
} else {
|
||||||
|
readValues[attribute] =
|
||||||
|
metadataInstance[attribute] ??
|
||||||
|
((metadataInstance.images || metadataInstance.others || [])[0] || {})[
|
||||||
|
attribute
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Test',
|
||||||
|
attribute,
|
||||||
|
readValues[attribute],
|
||||||
|
JSON.stringify(rule.constraint)
|
||||||
|
);
|
||||||
// Format the constraint as required by Validate.js
|
// Format the constraint as required by Validate.js
|
||||||
const testConstraint = {
|
const testConstraint = {
|
||||||
[attribute]: rule.constraint,
|
[attribute]: rule.constraint,
|
||||||
@ -35,13 +57,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
|||||||
|
|
||||||
// Create a single attribute object to be validated, since metadataInstance is an
|
// Create a single attribute object to be validated, since metadataInstance is an
|
||||||
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
||||||
let attributeValue = metadataInstance[attribute];
|
let attributeValue = readValues[attribute];
|
||||||
if (attributeValue === undefined) {
|
|
||||||
if (attribute === 'NumberOfStudyRelatedSeries') {
|
|
||||||
attributeValue = metadataInstance.series?.length;
|
|
||||||
}
|
|
||||||
// Add other computable values such as modalities in study
|
|
||||||
}
|
|
||||||
const attributeMap = {
|
const attributeMap = {
|
||||||
[attribute]: attributeValue,
|
[attribute]: attributeValue,
|
||||||
};
|
};
|
||||||
@ -49,7 +65,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
|||||||
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
||||||
let errorMessages;
|
let errorMessages;
|
||||||
try {
|
try {
|
||||||
errorMessages = validate(attributeMap, testConstraint, [options]);
|
errorMessages = validate(attributeMap, testConstraint, [validateOptions]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorMessages = ['Something went wrong during validation.', e];
|
errorMessages = ['Something went wrong during validation.', e];
|
||||||
}
|
}
|
||||||
@ -58,8 +74,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
|||||||
// If no errorMessages were returned, then validation passed.
|
// If no errorMessages were returned, then validation passed.
|
||||||
|
|
||||||
// Add the rule's weight to the total score
|
// Add the rule's weight to the total score
|
||||||
score += parseInt(rule.weight, 10);
|
score += parseInt(rule.weight || 1, 10);
|
||||||
|
|
||||||
// Log that this rule passed in the matching details object
|
// Log that this rule passed in the matching details object
|
||||||
details.passed.push({
|
details.passed.push({
|
||||||
rule,
|
rule,
|
||||||
|
|||||||
@ -0,0 +1,166 @@
|
|||||||
|
import HangingProtocolServiceClass from './HangingProtocolService';
|
||||||
|
|
||||||
|
const testProtocol = {
|
||||||
|
id: 'test',
|
||||||
|
locked: true,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'Default',
|
||||||
|
createdDate: '2021-02-23T19:22:08.894Z',
|
||||||
|
modifiedDate: '2021-02-23T19:22:08.894Z',
|
||||||
|
availableTo: {},
|
||||||
|
editableBy: {},
|
||||||
|
toolGroupIds: ['ctToolGroup', 'ptToolGroup'],
|
||||||
|
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||||
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'wauZK2QNEfDPwcAQo',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'StudyDescription',
|
||||||
|
constraint: {
|
||||||
|
contains: {
|
||||||
|
value: 'PETCT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
|
name: 'default',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'displaySet',
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: 'CT',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'numImageFrames',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctAXIAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
customViewportOptions: {
|
||||||
|
initialScale: 2.5,
|
||||||
|
},
|
||||||
|
initialImageOptions: {
|
||||||
|
// index: 5,
|
||||||
|
preset: 'first', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'displaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
numberOfPriorsReferenced: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const studyMatch = {
|
||||||
|
StudyInstanceUID: 'studyMatch',
|
||||||
|
StudyDescription: 'A PETCT study type',
|
||||||
|
};
|
||||||
|
|
||||||
|
const displaySet1 = {
|
||||||
|
...studyMatch,
|
||||||
|
SeriesInstanceUID: 'ds1',
|
||||||
|
displaySetInstanceUID: 'displaySet1',
|
||||||
|
numImageFrames: 11,
|
||||||
|
Modality: 'CT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const displaySet2 = {
|
||||||
|
...displaySet1,
|
||||||
|
SeriesInstanceUID: 'ds2',
|
||||||
|
displaySetInstanceUID: 'displaySet2',
|
||||||
|
Modality: 'PT',
|
||||||
|
};
|
||||||
|
|
||||||
|
const displaySet3 = {
|
||||||
|
...displaySet1,
|
||||||
|
numImageFrames: 3,
|
||||||
|
displaySetInstanceUID: 'displaySet3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const studyMatchDisplaySets = [displaySet3, displaySet2, displaySet1];
|
||||||
|
|
||||||
|
describe('HangingProtocolService', () => {
|
||||||
|
const commandsManager = {};
|
||||||
|
const hps = new HangingProtocolServiceClass(commandsManager);
|
||||||
|
let initialScaling;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
hps.addProtocols([testProtocol]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has one protocol', () => {
|
||||||
|
expect(hps.getProtocols().length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('run', () => {
|
||||||
|
it('matches best image match', () => {
|
||||||
|
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
|
||||||
|
const {
|
||||||
|
hpAlreadyApplied,
|
||||||
|
viewportMatchDetails,
|
||||||
|
displaySetMatchDetails,
|
||||||
|
} = hps.getMatchDetails();
|
||||||
|
expect(hpAlreadyApplied).toMatchObject([false]);
|
||||||
|
expect(viewportMatchDetails.length).toBe(1);
|
||||||
|
expect(viewportMatchDetails[0]).toMatchObject({
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctAXIAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
},
|
||||||
|
// Matches ds1 because it matches 2 rules, a required and an optional
|
||||||
|
// ds2 fails to match required and ds3 fails to match an optional.
|
||||||
|
displaySetsInfo: [
|
||||||
|
{
|
||||||
|
SeriesInstanceUID: 'ds1',
|
||||||
|
displaySetInstanceUID: 'displaySet1',
|
||||||
|
displaySetOptions: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,53 +1,110 @@
|
|||||||
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||||
import sortBy from '../../utils/sortBy.js';
|
import sortBy from '../../utils/sortBy';
|
||||||
import ProtocolEngine from './ProtocolEngine';
|
import ProtocolEngine from './ProtocolEngine';
|
||||||
|
import StudyMetadata from '../DicomMetadataStore/StudyMetadata';
|
||||||
|
import IDisplaySet from '../DisplaySetService/IDisplaySet';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
||||||
|
PROTOCOL_CHANGED: 'event::hanging_protocol_changed',
|
||||||
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
|
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
|
||||||
CUSTOM_IMAGE_LOAD_PERFORMED:
|
CUSTOM_IMAGE_LOAD_PERFORMED:
|
||||||
'event::hanging_protocol_custom_image_load_performed',
|
'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[];
|
||||||
|
};
|
||||||
|
|
||||||
class HangingProtocolService {
|
class HangingProtocolService {
|
||||||
constructor(commandsManager) {
|
studies: StudyMetadata[];
|
||||||
this._commandsManager = commandsManager;
|
protocols: Record<string, unknown>[];
|
||||||
this.protocols = [];
|
protocol: Record<string, unknown>;
|
||||||
this.ProtocolEngine = undefined;
|
stage: number;
|
||||||
this.protocol = undefined;
|
_commandsManager: Record<string, unknown>;
|
||||||
this.stage = undefined;
|
protocolEngine: ProtocolEngine;
|
||||||
|
hpAlreadyApplied: boolean[] = [];
|
||||||
|
customViewportSettings = [];
|
||||||
|
displaySets: IDisplaySet[] = [];
|
||||||
|
activeStudy: Record<string, unknown>;
|
||||||
|
debugLogging: false;
|
||||||
|
|
||||||
|
customAttributeRetrievalCallbacks = {
|
||||||
|
NumberOfStudyRelatedSeries: {
|
||||||
|
name: 'The number of series in the study',
|
||||||
|
callback: metadata =>
|
||||||
|
metadata.NumberOfStudyRelatedSeries ?? metadata.series?.length,
|
||||||
|
},
|
||||||
|
NumberOfSeriesRelatedInstances: {
|
||||||
|
name: 'The number of instances in the display set',
|
||||||
|
callback: metadata => metadata.numImageFrames,
|
||||||
|
},
|
||||||
|
ModalitiesInStudy: {
|
||||||
|
name: 'Gets the array of the modalities for the series',
|
||||||
|
callback: metadata =>
|
||||||
|
metadata.ModalitiesInStudy ??
|
||||||
|
(metadata.series || []).reduce((prev, curr) => {
|
||||||
|
const { Modality } = curr;
|
||||||
|
if (Modality && prev.indexOf(Modality) == -1) prev.push(Modality);
|
||||||
|
return prev;
|
||||||
|
}, []),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
listeners = {};
|
||||||
|
registeredImageLoadStrategies = {};
|
||||||
|
activeImageLoadStrategyName = null;
|
||||||
|
customImageLoadPerformed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* displaySetMatchDetails = <displaySetId, match>
|
||||||
|
* 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();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*
|
|
||||||
* {
|
|
||||||
* viewportOptions,
|
|
||||||
* displaySetsInfo, // contains array of [ { SeriesInstanceUID, displaySetOPtions}, ... ]
|
|
||||||
* }
|
|
||||||
*/
|
*/
|
||||||
this.matchDetails = [];
|
viewportMatchDetails = [] as ViewportMatchDetails[];
|
||||||
/**
|
|
||||||
* displaySetMatchDetails = <displaySetId, match>
|
constructor(commandsManager) {
|
||||||
* DisplaySetId is the id defined in the hangingProtocol
|
this._commandsManager = commandsManager;
|
||||||
* match is an object that contains information about
|
this.protocols = [];
|
||||||
*
|
this.protocolEngine = undefined;
|
||||||
* {
|
this.protocol = undefined;
|
||||||
* SeriesInstanceUID,
|
this.stage = undefined;
|
||||||
* StudyInstanceUID,
|
|
||||||
* matchDetails,
|
|
||||||
* matchingScore,
|
|
||||||
* sortingInfo
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.displaySetMatchDetails = new Map();
|
|
||||||
this.hpAlreadyApplied = [];
|
|
||||||
this.studies = [];
|
this.studies = [];
|
||||||
this.customViewportSettings = [];
|
|
||||||
this.customAttributeRetrievalCallbacks = {};
|
|
||||||
this.listeners = {};
|
|
||||||
this.registeredImageLoadStrategies = {};
|
|
||||||
this.activeImageLoadStrategyName = null;
|
|
||||||
this.customImageLoadPerformed = false;
|
|
||||||
Object.defineProperty(this, 'EVENTS', {
|
Object.defineProperty(this, 'EVENTS', {
|
||||||
value: EVENTS,
|
value: EVENTS,
|
||||||
writable: false,
|
writable: false,
|
||||||
@ -57,27 +114,27 @@ class HangingProtocolService {
|
|||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
public reset() {
|
||||||
this.studies = [];
|
this.studies = [];
|
||||||
this.protocols = [];
|
this.protocols = [];
|
||||||
this.hpAlreadyApplied = [];
|
this.hpAlreadyApplied = [];
|
||||||
this.matchDetails = [];
|
this.viewportMatchDetails = [];
|
||||||
// this.ProtocolEngine.reset()
|
// this.ProtocolEngine.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplaySetsMatchDetails() {
|
public getMatchDetails(): HangingProtocolMatchDetails {
|
||||||
return this.displaySetMatchDetails;
|
return {
|
||||||
|
viewportMatchDetails: this.viewportMatchDetails,
|
||||||
|
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||||
|
hpAlreadyApplied: this.hpAlreadyApplied,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getState() {
|
public getProtocols() {
|
||||||
return [this.matchDetails, this.hpAlreadyApplied];
|
|
||||||
}
|
|
||||||
|
|
||||||
getProtocols() {
|
|
||||||
return this.protocols;
|
return this.protocols;
|
||||||
}
|
}
|
||||||
|
|
||||||
addProtocols(protocols) {
|
public addProtocols(protocols) {
|
||||||
protocols.forEach(protocol => {
|
protocols.forEach(protocol => {
|
||||||
if (this.protocols.indexOf(protocol) === -1) {
|
if (this.protocols.indexOf(protocol) === -1) {
|
||||||
this.protocols.push(this._validateProtocol(protocol));
|
this.protocols.push(this._validateProtocol(protocol));
|
||||||
@ -85,21 +142,37 @@ class HangingProtocolService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
run(studyMetaData, protocol) {
|
/**
|
||||||
if (!this.studies.includes(studyMetaData)) {
|
* Run the hanging protocol decisions tree on the active study,
|
||||||
this.studies.push(studyMetaData);
|
* studies list and display sets, firing a hanging protocol event when
|
||||||
}
|
* complete to indicate the hanging protocol is ready.
|
||||||
// copy here so we don't mutate it
|
*
|
||||||
const metaData = Object.assign({}, studyMetaData);
|
* @param params is the dataset to run the hanging protocol on.
|
||||||
|
* @param params.activeStudy is the "primary" study to hang This may or may
|
||||||
|
* not be displayed by the actual viewports.
|
||||||
|
* @param params.studies is the list of studies to hang
|
||||||
|
* @param params.displaySets is the list of display sets associated with
|
||||||
|
* the studies to display in viewports.
|
||||||
|
* @param protocol is a specific protocol to apply.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public run({ studies, displaySets, activeStudy }, protocol) {
|
||||||
|
this.studies = [...studies];
|
||||||
|
this.displaySets = displaySets;
|
||||||
|
this.activeStudy = activeStudy || studies[0];
|
||||||
|
|
||||||
this.ProtocolEngine = new ProtocolEngine(
|
this.protocolEngine = new ProtocolEngine(
|
||||||
this.protocols,
|
this.protocols,
|
||||||
this.customAttributeRetrievalCallbacks
|
this.customAttributeRetrievalCallbacks
|
||||||
);
|
);
|
||||||
|
|
||||||
// if there is no pre-defined protocol
|
// if there is no pre-defined protocol
|
||||||
if (!protocol || protocol.id === undefined) {
|
if (!protocol || protocol.id === undefined) {
|
||||||
const matchedProtocol = this.ProtocolEngine.run(metaData);
|
const matchedProtocol = this.protocolEngine.run({
|
||||||
|
studies: this.studies,
|
||||||
|
activeStudy,
|
||||||
|
displaySets,
|
||||||
|
});
|
||||||
this._setProtocol(matchedProtocol);
|
this._setProtocol(matchedProtocol);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -112,7 +185,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
|
||||||
*/
|
*/
|
||||||
hasCustomImageLoadStrategy() {
|
public hasCustomImageLoadStrategy() {
|
||||||
return (
|
return (
|
||||||
this.activeImageLoadStrategyName !== null &&
|
this.activeImageLoadStrategyName !== null &&
|
||||||
this.registeredImageLoadStrategies[
|
this.registeredImageLoadStrategies[
|
||||||
@ -121,7 +194,7 @@ class HangingProtocolService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCustomImageLoadPerformed() {
|
public getCustomImageLoadPerformed() {
|
||||||
return this.customImageLoadPerformed;
|
return this.customImageLoadPerformed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,13 +203,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
|
||||||
*/
|
*/
|
||||||
registerImageLoadStrategy(name, callback) {
|
public registerImageLoadStrategy(name, callback) {
|
||||||
if (callback instanceof Function && name) {
|
if (callback instanceof Function && name) {
|
||||||
this.registeredImageLoadStrategies[name] = callback;
|
this.registeredImageLoadStrategies[name] = callback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHangingProtocolAppliedForViewport(i) {
|
public setHangingProtocolAppliedForViewport(i) {
|
||||||
this.hpAlreadyApplied[i] = true;
|
this.hpAlreadyApplied[i] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,18 +220,21 @@ class HangingProtocolService {
|
|||||||
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
||||||
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
addCustomAttribute(attributeId, attributeName, callback) {
|
public addCustomAttribute(attributeId, attributeName, callback, options) {
|
||||||
this.customAttributeRetrievalCallbacks[attributeId] = {
|
this.customAttributeRetrievalCallbacks[attributeId] = {
|
||||||
|
...options,
|
||||||
|
id: attributeId,
|
||||||
name: attributeName,
|
name: attributeName,
|
||||||
callback: callback,
|
callback,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches to the next protocol stage in the display set sequence
|
* Switches to the next protocol stage in the display set sequence
|
||||||
*/
|
*/
|
||||||
nextProtocolStage() {
|
public nextProtocolStage() {
|
||||||
console.log('ProtocolEngine::nextProtocolStage');
|
console.log('ProtocolEngine::nextProtocolStage');
|
||||||
|
|
||||||
if (!this._setCurrentProtocolStage(1)) {
|
if (!this._setCurrentProtocolStage(1)) {
|
||||||
@ -169,7 +245,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
|
||||||
*/
|
*/
|
||||||
previousProtocolStage() {
|
public previousProtocolStage() {
|
||||||
console.log('ProtocolEngine::previousProtocolStage');
|
console.log('ProtocolEngine::previousProtocolStage');
|
||||||
|
|
||||||
if (!this._setCurrentProtocolStage(-1)) {
|
if (!this._setCurrentProtocolStage(-1)) {
|
||||||
@ -187,8 +263,8 @@ class HangingProtocolService {
|
|||||||
];
|
];
|
||||||
const loadedData = loader({
|
const loadedData = loader({
|
||||||
data,
|
data,
|
||||||
displaySetsMatchDetails: this.getDisplaySetsMatchDetails(),
|
displaySetsMatchDetails: this.displaySetMatchDetails,
|
||||||
matchDetails: this.matchDetails,
|
viewportMatchDetails: this.viewportMatchDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
// if loader successfully re-arranged the data with the custom strategy
|
// if loader successfully re-arranged the data with the custom strategy
|
||||||
@ -253,6 +329,12 @@ class HangingProtocolService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._updateViewports();
|
this._updateViewports();
|
||||||
|
|
||||||
|
this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, {
|
||||||
|
viewportMatchDetails: this.viewportMatchDetails,
|
||||||
|
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||||
|
hpAlreadyApplied: this.hpAlreadyApplied,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -280,9 +362,13 @@ class HangingProtocolService {
|
|||||||
return this.protocol.stages[this.stage];
|
return this.protocol.stages[this.stage];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the viewports with the selected protocol stage.
|
||||||
|
*/
|
||||||
_updateViewports() {
|
_updateViewports() {
|
||||||
// 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');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,6 +387,7 @@ class HangingProtocolService {
|
|||||||
!stageModel.displaySets ||
|
!stageModel.displaySets ||
|
||||||
!stageModel.viewports.length
|
!stageModel.viewports.length
|
||||||
) {
|
) {
|
||||||
|
console.log('Stage cannot be applied', stageModel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,6 +398,7 @@ class HangingProtocolService {
|
|||||||
// If no such layout properties exist, stop here.
|
// If no such layout properties exist, stop here.
|
||||||
const layoutProps = stageModel.viewportStructure.properties;
|
const layoutProps = stageModel.viewportStructure.properties;
|
||||||
if (!layoutProps) {
|
if (!layoutProps) {
|
||||||
|
console.log('No viewportStructure.properties in', stageModel);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,12 +412,13 @@ class HangingProtocolService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Matching the displaySets
|
// Matching the displaySets
|
||||||
// Note: this is happening before displaySets are created. Here, displaySet
|
|
||||||
// only contains the information of the id of the displaySet to be matched
|
|
||||||
// based on some rules
|
|
||||||
stageModel.displaySets.forEach(displaySet => {
|
stageModel.displaySets.forEach(displaySet => {
|
||||||
const { bestMatch } = this._matchImages(displaySet);
|
const { bestMatch, matchingScores } = this._matchImages(displaySet);
|
||||||
this.displaySetMatchDetails.set(displaySet.id, bestMatch);
|
this.displaySetMatchDetails.set(displaySet.id, bestMatch);
|
||||||
|
if (bestMatch) {
|
||||||
|
bestMatch.matchingScores = matchingScores;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Loop through each viewport
|
// Loop through each viewport
|
||||||
@ -339,15 +428,26 @@ class HangingProtocolService {
|
|||||||
|
|
||||||
// DisplaySets for the viewport, Note: this is not the actual displaySet,
|
// DisplaySets for the viewport, Note: this is not the actual displaySet,
|
||||||
// but it is a info to locate the displaySet from the displaySetService
|
// but it is a info to locate the displaySet from the displaySetService
|
||||||
let displaySetsInfo = [];
|
const displaySetsInfo = [];
|
||||||
viewport.displaySets.forEach(({ id, options: displaySetOptions }) => {
|
viewport.displaySets.forEach(
|
||||||
const viewportDisplaySet = this.displaySetMatchDetails.get(id);
|
({ id, displaySetIndex = 0, options: displaySetOptions }) => {
|
||||||
|
const viewportDisplaySetMain = this.displaySetMatchDetails.get(id);
|
||||||
|
// Use the display set index to allow getting the "next" match, eg
|
||||||
|
// matching all display sets, and get the displaySetIndex'th item
|
||||||
|
const viewportDisplaySet =
|
||||||
|
!viewportDisplaySetMain || displaySetIndex === 0
|
||||||
|
? viewportDisplaySetMain
|
||||||
|
: viewportDisplaySetMain.matchingScores[displaySetIndex];
|
||||||
|
|
||||||
if (viewportDisplaySet) {
|
if (viewportDisplaySet) {
|
||||||
const { SeriesInstanceUID } = viewportDisplaySet;
|
const {
|
||||||
|
SeriesInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
} = viewportDisplaySet;
|
||||||
|
|
||||||
const displaySetInfo = {
|
const displaySetInfo = {
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
displaySetOptions,
|
displaySetOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -360,9 +460,10 @@ class HangingProtocolService {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.matchDetails[viewportIndex] = {
|
this.viewportMatchDetails[viewportIndex] = {
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
displaySetsInfo,
|
displaySetsInfo,
|
||||||
};
|
};
|
||||||
@ -370,50 +471,77 @@ class HangingProtocolService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Match images given a list of Studies and a Viewport's image matching reqs
|
// Match images given a list of Studies and a Viewport's image matching reqs
|
||||||
_matchImages(displaySet) {
|
_matchImages(displaySetRules) {
|
||||||
console.log('ProtocolEngine::matchImages');
|
|
||||||
|
|
||||||
// TODO: matching is applied on study and series level, instance
|
// TODO: matching is applied on study and series level, instance
|
||||||
// level matching needs to be added in future
|
// level matching needs to be added in future
|
||||||
|
|
||||||
// Todo: handle fusion viewports by not taking the first displaySet rule for the viewport
|
// Todo: handle fusion viewports by not taking the first displaySet rule for the viewport
|
||||||
const { studyMatchingRules, seriesMatchingRules } = displaySet;
|
const {
|
||||||
|
studyMatchingRules = [],
|
||||||
|
seriesMatchingRules,
|
||||||
|
findAll = false,
|
||||||
|
} = displaySetRules;
|
||||||
|
|
||||||
const matchingScores = [];
|
const matchingScores = [];
|
||||||
let highestStudyMatchingScore = 0;
|
let highestStudyMatchingScore = 0;
|
||||||
let highestSeriesMatchingScore = 0;
|
let highestSeriesMatchingScore = 0;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'ProtocolEngine::matchImages',
|
||||||
|
studyMatchingRules,
|
||||||
|
seriesMatchingRules
|
||||||
|
);
|
||||||
this.studies.forEach(study => {
|
this.studies.forEach(study => {
|
||||||
const studyMatchDetails = this.ProtocolEngine.findMatch(
|
const studyDisplaySets = this.displaySets.filter(
|
||||||
|
it => it.StudyInstanceUID === study.StudyInstanceUID
|
||||||
|
);
|
||||||
|
const studyMatchDetails = this.protocolEngine.findMatch(
|
||||||
study,
|
study,
|
||||||
studyMatchingRules
|
studyMatchingRules,
|
||||||
|
{ studies: this.studies, displaySets: studyDisplaySets }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||||
if (
|
if (studyMatchDetails.requiredFailed === true) {
|
||||||
studyMatchDetails.requiredFailed === true ||
|
|
||||||
studyMatchDetails.score < highestStudyMatchingScore
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
highestStudyMatchingScore = studyMatchDetails.score;
|
highestStudyMatchingScore = studyMatchDetails.score;
|
||||||
|
|
||||||
study.series.forEach(aSeries => {
|
this.debug(
|
||||||
const seriesMatchDetails = this.ProtocolEngine.findMatch(
|
'study',
|
||||||
aSeries,
|
study.StudyInstanceUID,
|
||||||
seriesMatchingRules
|
'display sets #',
|
||||||
|
this.displaySets.length
|
||||||
|
);
|
||||||
|
this.displaySets.forEach(displaySet => {
|
||||||
|
const {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
} = displaySet;
|
||||||
|
if (StudyInstanceUID !== study.StudyInstanceUID) return;
|
||||||
|
const seriesMatchDetails = this.protocolEngine.findMatch(
|
||||||
|
displaySet,
|
||||||
|
seriesMatchingRules,
|
||||||
|
{ studies: this.studies, instance: displaySet.images?.[0] }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||||
if (
|
if (seriesMatchDetails.requiredFailed === true) {
|
||||||
seriesMatchDetails.requiredFailed === true ||
|
this.debug(
|
||||||
seriesMatchDetails.score < highestSeriesMatchingScore
|
'Display set required failed',
|
||||||
) {
|
displaySet,
|
||||||
|
seriesMatchingRules
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
highestSeriesMatchingScore = seriesMatchDetails.score;
|
this.debug('Found displaySet for rules', displaySet);
|
||||||
|
highestSeriesMatchingScore = Math.max(
|
||||||
|
seriesMatchDetails.score,
|
||||||
|
highestSeriesMatchingScore
|
||||||
|
);
|
||||||
|
|
||||||
const matchDetails = {
|
const matchDetails = {
|
||||||
passed: [],
|
passed: [],
|
||||||
@ -438,21 +566,27 @@ class HangingProtocolService {
|
|||||||
seriesMatchDetails.score + studyMatchDetails.score;
|
seriesMatchDetails.score + studyMatchDetails.score;
|
||||||
|
|
||||||
const imageDetails = {
|
const imageDetails = {
|
||||||
StudyInstanceUID: study.StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID: aSeries.SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
matchingScore: totalMatchScore,
|
matchingScore: totalMatchScore,
|
||||||
matchDetails: matchDetails,
|
matchDetails: matchDetails,
|
||||||
sortingInfo: {
|
sortingInfo: {
|
||||||
score: totalMatchScore,
|
score: totalMatchScore,
|
||||||
study: study.StudyInstanceUID,
|
study: study.StudyInstanceUID,
|
||||||
series: parseInt(aSeries.SeriesNumber),
|
series: parseInt(displaySet.SeriesNumber),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.debug('Adding display set', displaySet, imageDetails);
|
||||||
matchingScores.push(imageDetails);
|
matchingScores.push(imageDetails);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (matchingScores.length === 0) {
|
||||||
|
console.log('No match found');
|
||||||
|
}
|
||||||
|
|
||||||
// Sort the matchingScores
|
// Sort the matchingScores
|
||||||
const sortingFunction = sortBy(
|
const sortingFunction = sortBy(
|
||||||
{
|
{
|
||||||
@ -473,7 +607,11 @@ class HangingProtocolService {
|
|||||||
|
|
||||||
const bestMatch = matchingScores[0];
|
const bestMatch = matchingScores[0];
|
||||||
|
|
||||||
console.log('ProtocolEngine::matchImages bestMatch', bestMatch);
|
console.log(
|
||||||
|
'ProtocolEngine::matchImages bestMatch',
|
||||||
|
bestMatch,
|
||||||
|
matchingScores
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
bestMatch,
|
bestMatch,
|
||||||
@ -495,7 +633,7 @@ class HangingProtocolService {
|
|||||||
* Check if the previous stage is available
|
* Check if the previous stage is available
|
||||||
* @return {Boolean} True if previous stage is available or false otherwise
|
* @return {Boolean} True if previous stage is available or false otherwise
|
||||||
*/
|
*/
|
||||||
_isPreviousStageAvailable() {
|
_isPreviousStageAvailable(): boolean {
|
||||||
return this.stage - 1 >= 0;
|
return this.stage - 1 >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,7 +644,7 @@ class HangingProtocolService {
|
|||||||
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
||||||
* @return {Boolean} True if new stage has set or false, otherwise
|
* @return {Boolean} True if new stage has set or false, otherwise
|
||||||
*/
|
*/
|
||||||
_setCurrentProtocolStage(stageAction) {
|
_setCurrentProtocolStage(stageAction): boolean {
|
||||||
//reseting the applied protocols
|
//reseting the applied protocols
|
||||||
this.hpAlreadyApplied = [];
|
this.hpAlreadyApplied = [];
|
||||||
// Check if previous or next stage is available
|
// Check if previous or next stage is available
|
||||||
@ -520,9 +658,7 @@ class HangingProtocolService {
|
|||||||
this.stage += stageAction;
|
this.stage += stageAction;
|
||||||
|
|
||||||
// Log the new stage
|
// Log the new stage
|
||||||
console.log(
|
this.debug(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
|
||||||
`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Since stage has changed, we need to update the viewports
|
// Since stage has changed, we need to update the viewports
|
||||||
// and redo matchings
|
// and redo matchings
|
||||||
@ -530,11 +666,22 @@ class HangingProtocolService {
|
|||||||
|
|
||||||
// Everything went well
|
// Everything went well
|
||||||
this._broadcastChange(this.EVENTS.STAGE_CHANGE, {
|
this._broadcastChange(this.EVENTS.STAGE_CHANGE, {
|
||||||
matchDetails: this.matchDetails,
|
viewportMatchDetails: this.viewportMatchDetails,
|
||||||
hpAlreadyApplied: this.hpAlreadyApplied,
|
hpAlreadyApplied: this.hpAlreadyApplied,
|
||||||
|
displaySetMatchDetails: this.displaySetMatchDetails,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Set this.debugLogging to true to show debug level logging - needed
|
||||||
|
* to be able to figure out why hanging protocols are or are not applying.
|
||||||
|
*/
|
||||||
|
debug(...args): void {
|
||||||
|
if (this.debugLogging) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts hanging protocols changes.
|
* Broadcasts hanging protocols changes.
|
||||||
*
|
*
|
||||||
@ -10,8 +10,15 @@ export default class ProtocolEngine {
|
|||||||
this.study = undefined;
|
this.study = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
run(studyMetaData) {
|
/** Evaluate the hanging protocol matches on the given:
|
||||||
this.study = studyMetaData;
|
* @param props.studies is a list of studies to compare against (for priors evaluation)
|
||||||
|
* @param props.activeStudy is the current metadata for the study to display.
|
||||||
|
* @param props.displaySets are the list of display sets which can be modified.
|
||||||
|
*/
|
||||||
|
run({ studies, displaySets, activeStudy }) {
|
||||||
|
this.studies = studies;
|
||||||
|
this.study = activeStudy || studies[0];
|
||||||
|
this.displaySets = displaySets;
|
||||||
return this.getBestProtocolMatch();
|
return this.getBestProtocolMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,9 +56,11 @@ export default class ProtocolEngine {
|
|||||||
// Clear all data currently in matchedProtocols
|
// Clear all data currently in matchedProtocols
|
||||||
this._clearMatchedProtocols();
|
this._clearMatchedProtocols();
|
||||||
|
|
||||||
// TODO: handle more than one study
|
// TODO: handle more than one study - this.studies has the list of studies
|
||||||
const study = this.study;
|
const matched = this.findMatchByStudy(this.study, {
|
||||||
const matched = this.findMatchByStudy(study);
|
studies: this.studies,
|
||||||
|
displaySets: this.displaySets,
|
||||||
|
});
|
||||||
|
|
||||||
// For each matched protocol, check if it is already in MatchedProtocols
|
// For each matched protocol, check if it is already in MatchedProtocols
|
||||||
matched.forEach(matchedDetail => {
|
matched.forEach(matchedDetail => {
|
||||||
@ -72,11 +81,12 @@ export default class ProtocolEngine {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findMatch(metaData, rules) {
|
findMatch(metaData, rules, options) {
|
||||||
return HPMatcher.match(
|
return HPMatcher.match(
|
||||||
metaData,
|
metaData,
|
||||||
rules,
|
rules,
|
||||||
this.customAttributeRetrievalCallbacks
|
this.customAttributeRetrievalCallbacks,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,11 +94,12 @@ export default class ProtocolEngine {
|
|||||||
* Finds the best protocols from Protocol Store, matching each protocol matching rules
|
* Finds the best protocols from Protocol Store, matching each protocol matching rules
|
||||||
* with the given study. The best protocol are ordered by score and returned in an array
|
* with the given study. The best protocol are ordered by score and returned in an array
|
||||||
* @param {Object} study StudyMetadata instance object
|
* @param {Object} study StudyMetadata instance object
|
||||||
|
* @param {object} options containing additional matching data.
|
||||||
* @return {Array} Array of match objects or an empty array if no match was found
|
* @return {Array} Array of match objects or an empty array if no match was found
|
||||||
* Each match object has the score of the matching and the matched
|
* Each match object has the score of the matching and the matched
|
||||||
* protocol
|
* protocol
|
||||||
*/
|
*/
|
||||||
findMatchByStudy(study) {
|
findMatchByStudy(study, options) {
|
||||||
const matched = [];
|
const matched = [];
|
||||||
|
|
||||||
this.protocols.forEach(protocol => {
|
this.protocols.forEach(protocol => {
|
||||||
@ -98,13 +109,14 @@ export default class ProtocolEngine {
|
|||||||
let rules = protocol.protocolMatchingRules.slice();
|
let rules = protocol.protocolMatchingRules.slice();
|
||||||
if (!rules || !rules.length) {
|
if (!rules || !rules.length) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules'
|
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules',
|
||||||
|
protocol.id
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the matcher and get matching details
|
// Run the matcher and get matching details
|
||||||
const matchedDetails = this.findMatch(study, rules);
|
const matchedDetails = this.findMatch(study, rules, options);
|
||||||
const score = matchedDetails.score;
|
const score = matchedDetails.score;
|
||||||
|
|
||||||
// The protocol matched some rule, add it to the matched list
|
// The protocol matched some rule, add it to the matched list
|
||||||
@ -156,605 +168,3 @@ export default class ProtocolEngine {
|
|||||||
return this.matchedProtocols.get(highestScoringProtocolId);
|
return this.matchedProtocols.get(highestScoringProtocolId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the ProtocolEngine to the best match
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current Stage from the current Protocol and stage index
|
|
||||||
*
|
|
||||||
* @returns {*} The Stage model for the currently displayed Stage
|
|
||||||
*/
|
|
||||||
// getCurrentStageModel() {
|
|
||||||
// return this.protocol.stages[this.stage];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Get the number of prior studies supplied in the priorStudies map property.
|
|
||||||
// *
|
|
||||||
// * @param {String} studyObjectID The study object ID of the study whose priors are needed
|
|
||||||
// * @returns {number} The number of available prior studies with the same PatientID
|
|
||||||
// */
|
|
||||||
// getNumberOfAvailablePriors(studyObjectID) {
|
|
||||||
// return this.getAvailableStudyPriors(studyObjectID).length;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Get the array of prior studies from a specific study.
|
|
||||||
// *
|
|
||||||
// * @param {String} studyObjectID The study object ID of the study whose priors are needed
|
|
||||||
// * @returns {Array} The array of available priors or an empty array
|
|
||||||
// */
|
|
||||||
// getAvailableStudyPriors(studyObjectID) {
|
|
||||||
// const priors = this.priorStudies.get(studyObjectID);
|
|
||||||
|
|
||||||
// return priors instanceof Array ? priors : [];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Match images given a list of Studies and a Viewport's image matching reqs
|
|
||||||
// matchImages(viewport, viewportIndex) {
|
|
||||||
// log.trace('ProtocolEngine::matchImages');
|
|
||||||
|
|
||||||
// const {
|
|
||||||
// studyMatchingRules,
|
|
||||||
// seriesMatchingRules,
|
|
||||||
// imageMatchingRules: instanceMatchingRules,
|
|
||||||
// } = viewport;
|
|
||||||
|
|
||||||
// const matchingScores = [];
|
|
||||||
// const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
|
|
||||||
// const firstInstance = currentStudy.getFirstInstance();
|
|
||||||
|
|
||||||
// let highestStudyMatchingScore = 0;
|
|
||||||
// let highestSeriesMatchingScore = 0;
|
|
||||||
|
|
||||||
// // Set custom attribute for study metadata and it's first instance
|
|
||||||
// currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
|
||||||
// if (firstInstance instanceof InstanceMetadata) {
|
|
||||||
// firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Only used if study matching rules has abstract prior values defined...
|
|
||||||
// let priorStudies;
|
|
||||||
|
|
||||||
// studyMatchingRules.forEach(rule => {
|
|
||||||
// if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
|
|
||||||
// const validatorType = Object.keys(rule.constraint)[0];
|
|
||||||
// const validator = Object.keys(rule.constraint[validatorType])[0];
|
|
||||||
|
|
||||||
// let abstractPriorValue = rule.constraint[validatorType][validator];
|
|
||||||
// abstractPriorValue = parseInt(abstractPriorValue, 10);
|
|
||||||
// // TODO: Restrict or clarify validators for abstractPriorValue?
|
|
||||||
|
|
||||||
// // No need to call it more than once...
|
|
||||||
// if (!priorStudies) {
|
|
||||||
// priorStudies = this.getAvailableStudyPriors(
|
|
||||||
// currentStudy.getObjectID()
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TODO: Revisit this later: What about two studies with the same
|
|
||||||
// // study date?
|
|
||||||
|
|
||||||
// let priorStudy;
|
|
||||||
// if (abstractPriorValue === -1) {
|
|
||||||
// priorStudy = priorStudies[priorStudies.length - 1];
|
|
||||||
// } else {
|
|
||||||
// const studyIndex = Math.max(abstractPriorValue - 1, 0);
|
|
||||||
// priorStudy = priorStudies[studyIndex];
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Invalid data
|
|
||||||
// if (!priorStudy instanceof StudyMetadata) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const priorStudyObjectID = priorStudy.getObjectID();
|
|
||||||
|
|
||||||
// // Check if study metadata is already in studies list
|
|
||||||
// if (
|
|
||||||
// this.studies.find(study => study.getObjectID() === priorStudyObjectID)
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
|
|
||||||
// this.studyMetadataSource.loadStudy(priorStudy).then(
|
|
||||||
// studyMetadata => {
|
|
||||||
// // Set the custom attribute abstractPriorValue for the study metadata
|
|
||||||
// studyMetadata.setCustomAttribute(
|
|
||||||
// ABSTRACT_PRIOR_VALUE,
|
|
||||||
// abstractPriorValue
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Also add custom attribute
|
|
||||||
// const firstInstance = studyMetadata.getFirstInstance();
|
|
||||||
// if (firstInstance instanceof InstanceMetadata) {
|
|
||||||
// firstInstance.setCustomAttribute(
|
|
||||||
// ABSTRACT_PRIOR_VALUE,
|
|
||||||
// abstractPriorValue
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Insert the new study metadata
|
|
||||||
// this.studies.push(studyMetadata);
|
|
||||||
|
|
||||||
// // Update the viewport to refresh layout manager with new study
|
|
||||||
// this.updateViewports(viewportIndex);
|
|
||||||
// },
|
|
||||||
// error => {
|
|
||||||
// log.warn(error);
|
|
||||||
// throw new OHIFError(
|
|
||||||
// `ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// // TODO: Add relative Date / time
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.studies.forEach(study => {
|
|
||||||
// const studyMatchDetails = HPMatcher.match(
|
|
||||||
// study.getFirstInstance(),
|
|
||||||
// studyMatchingRules
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
|
||||||
// if (
|
|
||||||
// studyMatchDetails.requiredFailed === true ||
|
|
||||||
// studyMatchDetails.score < highestStudyMatchingScore
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// highestStudyMatchingScore = studyMatchDetails.score;
|
|
||||||
|
|
||||||
// study.forEachSeries(series => {
|
|
||||||
// const seriesMatchDetails = HPMatcher.match(
|
|
||||||
// series.getFirstInstance(),
|
|
||||||
// seriesMatchingRules
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
|
||||||
// if (
|
|
||||||
// seriesMatchDetails.requiredFailed === true ||
|
|
||||||
// seriesMatchDetails.score < highestSeriesMatchingScore
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// highestSeriesMatchingScore = seriesMatchDetails.score;
|
|
||||||
|
|
||||||
// series.forEachInstance((instance, index) => {
|
|
||||||
// // This tests to make sure there is actually image data in this instance
|
|
||||||
// // TODO: Change this when we add PDF and MPEG support
|
|
||||||
// // See https://ohiforg.atlassian.net/browse/LT-227
|
|
||||||
// if (
|
|
||||||
// !isImage(instance.getTagValue('SOPClassUID')) &&
|
|
||||||
// !instance.getTagValue('Rows')
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const instanceMatchDetails = HPMatcher.match(
|
|
||||||
// instance,
|
|
||||||
// instanceMatchingRules
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
|
||||||
// if (instanceMatchDetails.requiredFailed === true) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const matchDetails = {
|
|
||||||
// passed: [],
|
|
||||||
// failed: [],
|
|
||||||
// };
|
|
||||||
|
|
||||||
// matchDetails.passed = matchDetails.passed.concat(
|
|
||||||
// instanceMatchDetails.details.passed
|
|
||||||
// );
|
|
||||||
// matchDetails.passed = matchDetails.passed.concat(
|
|
||||||
// seriesMatchDetails.details.passed
|
|
||||||
// );
|
|
||||||
// matchDetails.passed = matchDetails.passed.concat(
|
|
||||||
// studyMatchDetails.details.passed
|
|
||||||
// );
|
|
||||||
|
|
||||||
// matchDetails.failed = matchDetails.failed.concat(
|
|
||||||
// instanceMatchDetails.details.failed
|
|
||||||
// );
|
|
||||||
// matchDetails.failed = matchDetails.failed.concat(
|
|
||||||
// seriesMatchDetails.details.failed
|
|
||||||
// );
|
|
||||||
// matchDetails.failed = matchDetails.failed.concat(
|
|
||||||
// studyMatchDetails.details.failed
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const totalMatchScore =
|
|
||||||
// instanceMatchDetails.score +
|
|
||||||
// seriesMatchDetails.score +
|
|
||||||
// studyMatchDetails.score;
|
|
||||||
// const currentSOPInstanceUID = instance.getSOPInstanceUID();
|
|
||||||
|
|
||||||
// const imageDetails = {
|
|
||||||
// StudyInstanceUID: study.getStudyInstanceUID(),
|
|
||||||
// SeriesInstanceUID: series.getSeriesInstanceUID(),
|
|
||||||
// SOPInstanceUID: currentSOPInstanceUID,
|
|
||||||
// currentImageIdIndex: index,
|
|
||||||
// matchingScore: totalMatchScore,
|
|
||||||
// matchDetails: matchDetails,
|
|
||||||
// sortingInfo: {
|
|
||||||
// score: totalMatchScore,
|
|
||||||
// study:
|
|
||||||
// instance.getTagValue('StudyDate') +
|
|
||||||
// instance.getTagValue('StudyTime'),
|
|
||||||
// series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime
|
|
||||||
// instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
|
|
||||||
// // Find the displaySet
|
|
||||||
// const displaySet = study.findDisplaySet(displaySet =>
|
|
||||||
// displaySet.images.find(
|
|
||||||
// image => image.getSOPInstanceUID() === currentSOPInstanceUID
|
|
||||||
// )
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // If the instance was found, set the displaySet ID
|
|
||||||
// if (displaySet) {
|
|
||||||
// imageDetails.displaySetInstanceUID = displaySet.getUID();
|
|
||||||
// imageDetails.imageId = instance.getImageId();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// matchingScores.push(imageDetails);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Sort the matchingScores
|
|
||||||
// const sortingFunction = sortBy(
|
|
||||||
// {
|
|
||||||
// name: 'score',
|
|
||||||
// reverse: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'study',
|
|
||||||
// reverse: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'instance',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'series',
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// matchingScores.sort((a, b) =>
|
|
||||||
// sortingFunction(a.sortingInfo, b.sortingInfo)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const bestMatch = matchingScores[0];
|
|
||||||
|
|
||||||
// log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// bestMatch,
|
|
||||||
// matchingScores,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the current layout
|
|
||||||
*
|
|
||||||
* @param {number} numRows
|
|
||||||
* @param {number} numColumns
|
|
||||||
*/
|
|
||||||
// setLayout(numRows, numColumns) {
|
|
||||||
// if (numRows < 1 && numColumns < 1) {
|
|
||||||
// log.error(`Invalid layout ${numRows} x ${numColumns}`);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (typeof this.options.setLayout !== 'function') {
|
|
||||||
// log.error('Hanging Protocol Engine setLayout callback is not defined');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let viewports = [];
|
|
||||||
// const numViewports = numRows * numColumns;
|
|
||||||
|
|
||||||
// for (let i = 0; i < numViewports; i++) {
|
|
||||||
// viewports.push({});
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.options.setLayout({ numRows, numColumns, viewports });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Rerenders viewports that are part of the current layout manager
|
|
||||||
// * using the matching rules internal to each viewport.
|
|
||||||
// *
|
|
||||||
// * If this function is provided the index of a viewport, only the specified viewport
|
|
||||||
// * is rerendered.
|
|
||||||
// *
|
|
||||||
// * @param viewportIndex
|
|
||||||
// */
|
|
||||||
// updateViewports(viewportIndex) {
|
|
||||||
// log.trace(
|
|
||||||
// `ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Make sure we have an active protocol with a non-empty array of display sets
|
|
||||||
// if (!this.getNumProtocolStages()) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Retrieve the current stage
|
|
||||||
// const stageModel = this.getCurrentStageModel();
|
|
||||||
|
|
||||||
// // If the current stage does not fulfill the requirements to be displayed,
|
|
||||||
// // stop here.
|
|
||||||
// if (
|
|
||||||
// !stageModel ||
|
|
||||||
// !stageModel.viewportStructure ||
|
|
||||||
// !stageModel.viewports ||
|
|
||||||
// !stageModel.viewports.length
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Retrieve the layoutTemplate associated with the current display set's viewport structure
|
|
||||||
// // If no such template name exists, stop here.
|
|
||||||
// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
|
||||||
// if (!layoutTemplateName) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Retrieve the properties associated with the current display set's viewport structure template
|
|
||||||
// // If no such layout properties exist, stop here.
|
|
||||||
// const layoutProps = stageModel.viewportStructure.properties;
|
|
||||||
// if (!layoutProps) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Create an empty array to store the output viewportData
|
|
||||||
// const viewportData = [];
|
|
||||||
|
|
||||||
// // Empty the matchDetails associated with the ProtocolEngine.
|
|
||||||
// // This will be used to store the pass/fail details and score
|
|
||||||
// // for each of the viewport matching procedures
|
|
||||||
// this.matchDetails = [];
|
|
||||||
|
|
||||||
// // Loop through each viewport
|
|
||||||
// stageModel.viewports.forEach((viewport, viewportIndex) => {
|
|
||||||
// const details = this.matchImages(viewport, viewportIndex);
|
|
||||||
|
|
||||||
// this.matchDetails[viewportIndex] = details;
|
|
||||||
|
|
||||||
// // Convert any YES/NO values into true/false for Cornerstone
|
|
||||||
// const cornerstoneViewportParams = {};
|
|
||||||
|
|
||||||
// // Cache viewportSettings keys
|
|
||||||
// const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
|
|
||||||
|
|
||||||
// viewportSettingsKeys.forEach(key => {
|
|
||||||
// let value = viewport.viewportSettings[key];
|
|
||||||
// if (value === 'YES') {
|
|
||||||
// value = true;
|
|
||||||
// } else if (value === 'NO') {
|
|
||||||
// value = false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// cornerstoneViewportParams[key] = value;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // imageViewerViewports occasionally needs relevant layout data in order to set
|
|
||||||
// // the element style of the viewport in question
|
|
||||||
// const currentViewportData = {
|
|
||||||
// viewportIndex,
|
|
||||||
// viewport: cornerstoneViewportParams,
|
|
||||||
// ...layoutProps,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const customSettings = [];
|
|
||||||
// viewportSettingsKeys.forEach(id => {
|
|
||||||
// const setting = CustomViewportSettings[id];
|
|
||||||
// if (!setting) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// customSettings.push({
|
|
||||||
// id: id,
|
|
||||||
// value: viewport.viewportSettings[id],
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
// currentViewportData.renderedCallback = element => {
|
|
||||||
// //console.log('renderedCallback for ' + element.id);
|
|
||||||
// customSettings.forEach(customSetting => {
|
|
||||||
// log.trace(
|
|
||||||
// `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
|
|
||||||
// );
|
|
||||||
// log.trace(
|
|
||||||
// `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const setting = CustomViewportSettings[customSetting.id];
|
|
||||||
// setting.callback(element, customSetting.value);
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let currentMatch = details.bestMatch;
|
|
||||||
// let currentPosition = 1;
|
|
||||||
// const scoresLength = details.matchingScores.length;
|
|
||||||
// while (
|
|
||||||
// currentPosition < scoresLength &&
|
|
||||||
// viewportData.find(a => a.imageId === currentMatch.imageId)
|
|
||||||
// ) {
|
|
||||||
// currentMatch = details.matchingScores[currentPosition];
|
|
||||||
// currentPosition++;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (currentMatch && currentMatch.imageId) {
|
|
||||||
// currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID;
|
|
||||||
// currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID;
|
|
||||||
// currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID;
|
|
||||||
// currentViewportData.currentImageIdIndex =
|
|
||||||
// currentMatch.currentImageIdIndex;
|
|
||||||
// currentViewportData.displaySetInstanceUID =
|
|
||||||
// currentMatch.displaySetInstanceUID;
|
|
||||||
// currentViewportData.imageId = currentMatch.imageId;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
|
|
||||||
// // if (!currentViewportData.displaySetInstanceUID) {
|
|
||||||
// // throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// viewportData.push(currentViewportData);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// this.setLayout(layoutProps.Rows, layoutProps.Columns);
|
|
||||||
|
|
||||||
// if (typeof this.options.setViewportSpecificData !== 'function') {
|
|
||||||
// log.error(
|
|
||||||
// 'Hanging Protocol Engine setViewportSpecificData callback is not defined'
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // If viewportIndex is defined, then update only that viewport
|
|
||||||
// if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
|
||||||
// this.options.setViewportSpecificData(
|
|
||||||
// viewportIndex,
|
|
||||||
// viewportData[viewportIndex]
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Update all viewports
|
|
||||||
// viewportData.forEach(viewportSpecificData => {
|
|
||||||
// this.options.setViewportSpecificData(
|
|
||||||
// viewportSpecificData.viewportIndex,
|
|
||||||
// viewportSpecificData
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Sets the current Hanging Protocol to the specified Protocol
|
|
||||||
// * An optional argument can also be used to prevent the updating of the Viewports
|
|
||||||
// *
|
|
||||||
// * @param newProtocol
|
|
||||||
// * @param updateViewports
|
|
||||||
// */
|
|
||||||
// setHangingProtocol(newProtocol, updateViewports = true) {
|
|
||||||
// log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
|
|
||||||
// log.trace(
|
|
||||||
// `ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // Reset the array of newStageIds
|
|
||||||
// this.newStageIds = [];
|
|
||||||
|
|
||||||
// if (Protocol.prototype.isPrototypeOf(newProtocol)) {
|
|
||||||
// this.protocol = newProtocol;
|
|
||||||
// } else {
|
|
||||||
// this.protocol = new Protocol();
|
|
||||||
// this.protocol.fromObject(newProtocol);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.stage = 0;
|
|
||||||
|
|
||||||
// // Update viewports by default
|
|
||||||
// if (updateViewports) {
|
|
||||||
// this.updateViewports();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Check if the next stage is available
|
|
||||||
// * @return {Boolean} True if next stage is available or false otherwise
|
|
||||||
// */
|
|
||||||
// isNextStageAvailable() {
|
|
||||||
// const numberOfStages = this.getNumProtocolStages();
|
|
||||||
|
|
||||||
// return this.stage + 1 < numberOfStages;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Check if the previous stage is available
|
|
||||||
// * @return {Boolean} True if previous stage is available or false otherwise
|
|
||||||
// */
|
|
||||||
// isPreviousStageAvailable() {
|
|
||||||
// return this.stage - 1 >= 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Changes the current stage to a new stage index in the display set sequence.
|
|
||||||
// * It checks if the next stage exists.
|
|
||||||
// *
|
|
||||||
// * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
|
||||||
// * @return {Boolean} True if new stage has set or false, otherwise
|
|
||||||
// */
|
|
||||||
// setCurrentProtocolStage(stageAction) {
|
|
||||||
// // Check if previous or next stage is available
|
|
||||||
// if (stageAction === -1 && !this.isPreviousStageAvailable()) {
|
|
||||||
// return false;
|
|
||||||
// } else if (stageAction === 1 && !this.isNextStageAvailable()) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Sets the new stage
|
|
||||||
// this.stage += stageAction;
|
|
||||||
|
|
||||||
// // Log the new stage
|
|
||||||
// log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
|
|
||||||
|
|
||||||
// // Since stage has changed, we need to update the viewports
|
|
||||||
// // and redo matchings
|
|
||||||
// this.updateViewports();
|
|
||||||
|
|
||||||
// // Everything went well
|
|
||||||
// return true;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Retrieves the number of Stages in the current Protocol or
|
|
||||||
// * undefined if no protocol or stages are set
|
|
||||||
// */
|
|
||||||
// getNumProtocolStages() {
|
|
||||||
// if (
|
|
||||||
// !this.protocol ||
|
|
||||||
// !this.protocol.stages ||
|
|
||||||
// !this.protocol.stages.length
|
|
||||||
// ) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return this.protocol.stages.length;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Switches to the next protocol stage in the display set sequence
|
|
||||||
// */
|
|
||||||
// nextProtocolStage() {
|
|
||||||
// log.trace('ProtocolEngine::nextProtocolStage');
|
|
||||||
|
|
||||||
// if (!this.setCurrentProtocolStage(1)) {
|
|
||||||
// log.trace('ProtocolEngine::nextProtocolStage failed');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Switches to the previous protocol stage in the display set sequence
|
|
||||||
// */
|
|
||||||
// previousProtocolStage() {
|
|
||||||
// log.trace('ProtocolEngine::previousProtocolStage');
|
|
||||||
|
|
||||||
// if (!this.setCurrentProtocolStage(-1)) {
|
|
||||||
// log.trace('ProtocolEngine::previousProtocolStage failed');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@ -1,39 +1,88 @@
|
|||||||
import validate from 'validate.js';
|
import validate from 'validate.js';
|
||||||
|
|
||||||
validate.validators.equals = function (value, options, key, attributes) {
|
validate.validators.equals = function(value, options, key, attributes) {
|
||||||
if (options && value !== options.value) {
|
const testValue = options?.value ?? options;
|
||||||
return key + 'must equal ' + options.value;
|
if (value !== testValue) {
|
||||||
|
return key + 'must equal ' + testValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
validate.validators.doesNotEqual = function (value, options, key) {
|
validate.validators.doesNotEqual = function(value, options, key) {
|
||||||
if (options && value === options.value) {
|
const testValue = options?.value ?? options;
|
||||||
return key + 'cannot equal ' + options.value;
|
if (value === testValue) {
|
||||||
|
return key + 'cannot equal ' + testValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
validate.validators.contains = function (value, options, key) {
|
validate.validators.contains = function(value, options, key) {
|
||||||
if (options && value.indexOf && value.indexOf(options.value) === -1) {
|
const testValue = options?.value ?? options;
|
||||||
return key + 'must contain ' + options.value;
|
if (Array.isArray(value)) {
|
||||||
|
if (value.some(item => !validate.validators.contains(item, options, key))) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `No item of ${value.join(',')} contains ${JSON.stringify(
|
||||||
|
testValue
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (Array.isArray(testValue)) {
|
||||||
|
if (
|
||||||
|
testValue.some(
|
||||||
|
subTest => !validate.validators.contains(value, subTest, key)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return `${key} must contain at least one of ${testValue.join(',')}`;
|
||||||
|
}
|
||||||
|
if (testValue && value.indexOf && value.indexOf(testValue) === -1) {
|
||||||
|
return key + 'must contain ' + testValue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
validate.validators.doesNotContain = function (value, options, key) {
|
validate.validators.doesNotContain = function(value, options, key) {
|
||||||
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
||||||
return key + 'cannot contain ' + options.value;
|
return key + 'cannot contain ' + options.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
validate.validators.startsWith = function (value, options, key) {
|
validate.validators.startsWith = function(value, options, key) {
|
||||||
if (options && value.startsWith && !value.startsWith(options.value)) {
|
if (options && value.startsWith && !value.startsWith(options.value)) {
|
||||||
return key + 'must start with ' + options.value;
|
return key + 'must start with ' + options.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
validate.validators.endsWith = function (value, options, key) {
|
validate.validators.endsWith = function(value, options, key) {
|
||||||
if (options && value.endsWith && !value.endsWith(options.value)) {
|
if (options && value.endsWith && !value.endsWith(options.value)) {
|
||||||
return key + 'must end with ' + options.value;
|
return key + 'must end with ' + options.value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
validate.validators.greaterThan = function(value, options, key) {
|
||||||
|
const testValue = options?.value ?? options;
|
||||||
|
if (testValue !== undefined && value <= testValue) {
|
||||||
|
return key + 'with value ' + value + ' must be greater than ' + testValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.range = function(value, options, key) {
|
||||||
|
const testValue = options?.value ?? options;
|
||||||
|
if (
|
||||||
|
(testValue !== undefined && value < testValue[0]) ||
|
||||||
|
value > testValue[1]
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
key +
|
||||||
|
'with value ' +
|
||||||
|
value +
|
||||||
|
' must be between ' +
|
||||||
|
testValue[0] +
|
||||||
|
' and ' +
|
||||||
|
testValue[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.notNull = value =>
|
||||||
|
value === null || value === undefined ? 'Value is null' : undefined;
|
||||||
|
|
||||||
export default validate;
|
export default validate;
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
import validate from "./validator.js";
|
||||||
|
|
||||||
|
describe("validator", () => {
|
||||||
|
const attributeMap = {
|
||||||
|
str: "string",
|
||||||
|
num: 3,
|
||||||
|
nullValue: null,
|
||||||
|
list: ["abc", "def"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
format: 'grouped',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("contains", () => {
|
||||||
|
it("returns match any list contains", () => {
|
||||||
|
expect(validate(attributeMap, { list: { contains: 'a' } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { str: { contains: 'i' } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { str: { contains: ['i'] } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { list: { contains: ['a'] } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { list: { contains: ['z', 'd'] } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { list: { contains: ['z'] } }, [options])).not.toBeUndefined();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("equals", () => {
|
||||||
|
it("returned undefined on equals", () => {
|
||||||
|
expect(validate(attributeMap, { str: { equals: attributeMap.str } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { num: { equals: { value: attributeMap.num } } }, [options])).toBeUndefined();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns error on not equals", () => {
|
||||||
|
expect(validate(attributeMap, { str: { equals: "abc" } }, [options])).not.toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { num: { equals: { value: 1 + attributeMap.num } } }, [options])).not.toBeUndefined();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("greaterThan", () => {
|
||||||
|
it("returns undefined on greaterThan", () => {
|
||||||
|
expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num - 1 } } }, [options])).toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { num: { greaterThan: attributeMap.num - 1 } }, [options])).toBeUndefined();
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns error on not greater than", () => {
|
||||||
|
expect(validate(attributeMap, { num: { greaterThan: { value: attributeMap.num } } }, [options])).not.toBeUndefined();
|
||||||
|
expect(validate(attributeMap, { num: { greaterThan: attributeMap.num } }, [options])).not.toBeUndefined();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
@ -10,41 +10,7 @@ sidebar_label: DisplaySet Service
|
|||||||
|
|
||||||
> Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series.
|
> Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series.
|
||||||
|
|
||||||
|
DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md).
|
||||||
```js title="platform/core/src/services/DisplaySetService/DisplaySetService.js"
|
|
||||||
init(extensionManager, SOPClassHandlerIds) {
|
|
||||||
this.extensionManager = extensionManager;
|
|
||||||
this.SOPClassHandlerIds = SOPClassHandlerIds;
|
|
||||||
this.activeDisplaySets = [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
in `Mode.jsx`
|
|
||||||
|
|
||||||
```js title="platform/viewer/src/routes/Mode/Mode.jsx"
|
|
||||||
export default function ModeRoute(/** ... **/) {
|
|
||||||
/** ... **/
|
|
||||||
const { DisplaySetService } = servicesManager.services
|
|
||||||
const { sopClassHandlers } = mode
|
|
||||||
/** ... **/
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
/** ... **/
|
|
||||||
|
|
||||||
// Add SOPClassHandlers to a new SOPClassManager.
|
|
||||||
DisplaySetService.init(extensionManager, sopClassHandlers)
|
|
||||||
|
|
||||||
/** ... **/
|
|
||||||
}
|
|
||||||
/** ... **/
|
|
||||||
)
|
|
||||||
/** ... **/
|
|
||||||
return <> /** ... **/ </>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
There are three events that get broadcasted in `DisplaySetService`:
|
There are three events that get broadcasted in `DisplaySetService`:
|
||||||
|
|||||||
@ -59,7 +59,6 @@ There are three events that get broadcasted in `DisplaySetService`:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
Let's find out about the public API for `DisplaySetService`.
|
Let's find out about the public API for `DisplaySetService`.
|
||||||
|
|
||||||
@ -83,3 +82,7 @@ Let's find out about the public API for `DisplaySetService`.
|
|||||||
- `getActiveDisplaySets`: Returns the active displaySets
|
- `getActiveDisplaySets`: Returns the active displaySets
|
||||||
|
|
||||||
- `deleteDisplaySet`: Deletes the displaySets from the displaySets cache
|
- `deleteDisplaySet`: Deletes the displaySets from the displaySets cache
|
||||||
|
|
||||||
|
- `holdChangeEvents`: Prevents firing change events (currently only works on add event).
|
||||||
|
|
||||||
|
- `fireHoldChangeEvents`: Causes the change event to be fired IF there were any changes. No longer holds events.
|
||||||
|
|||||||
@ -29,7 +29,7 @@ You can find the skeleton of the hanging protocols here:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const defaultProtocol = {
|
const defaultProtocol = {
|
||||||
id: 'defaultProtocol',
|
id: 'test',
|
||||||
locked: true,
|
locked: true,
|
||||||
hasUpdatedPriorsInformation: false,
|
hasUpdatedPriorsInformation: false,
|
||||||
name: 'Default',
|
name: 'Default',
|
||||||
@ -37,31 +37,91 @@ const defaultProtocol = {
|
|||||||
modifiedDate: '2021-02-23T19:22:08.894Z',
|
modifiedDate: '2021-02-23T19:22:08.894Z',
|
||||||
availableTo: {},
|
availableTo: {},
|
||||||
editableBy: {},
|
editableBy: {},
|
||||||
protocolMatchingRules: [],
|
toolGroupIds: [
|
||||||
|
'ctToolGroup',
|
||||||
|
'ptToolGroup',
|
||||||
|
],
|
||||||
|
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||||
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'wauZK2QNEfDPwcAQo',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'StudyDescription',
|
||||||
|
constraint: {
|
||||||
|
contains: {
|
||||||
|
value: 'PETCT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
id: 'nwzau7jDkEkL8djfr',
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
name: 'oneByOne',
|
name: 'default',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
type: 'grid',
|
layoutType: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
rows: 1,
|
rows: 1,
|
||||||
columns: 1,
|
columns: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
viewports: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
viewportSettings: [],
|
id: 'displaySet',
|
||||||
imageMatchingRules: [],
|
seriesMatchingRules: [
|
||||||
seriesMatchingRules: [],
|
{
|
||||||
|
id: 'GPEYqFLv2dwzCM322',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: 'CT',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'vSjk7NCYjtdS3XZAw',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'numImageFrames',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
studyMatchingRules: [],
|
studyMatchingRules: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
createdDate: '2021-02-23T19:22:08.894Z',
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctAXIAL',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
initialImageOptions: {
|
||||||
|
// index: 5,
|
||||||
|
preset: 'first', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'displaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
numberOfPriorsReferenced: -1,
|
numberOfPriorsReferenced: -1,
|
||||||
};
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's discuss each property in depth.
|
Let's discuss each property in depth.
|
||||||
@ -74,7 +134,7 @@ Let's discuss each property in depth.
|
|||||||
- `weight`: weight for the matching rule. Eventually, all the registered
|
- `weight`: weight for the matching rule. Eventually, all the registered
|
||||||
protocols get sorted based on the weights, and the winning protocol gets
|
protocols get sorted based on the weights, and the winning protocol gets
|
||||||
applied to the viewer.
|
applied to the viewer.
|
||||||
- `attriubte`: tag that needs to be matched against. This can be either
|
- `attribute`: tag that needs to be matched against. This can be either
|
||||||
Study-level metadata or a custom attribute.
|
Study-level metadata or a custom attribute.
|
||||||
[Learn more about custom attribute matching](#custom-attribute)
|
[Learn more about custom attribute matching](#custom-attribute)
|
||||||
|
|
||||||
@ -85,89 +145,62 @@ Let's discuss each property in depth.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
id: 'wauZK2QNEfDPwcAQo',
|
|
||||||
weight: 1,
|
weight: 1,
|
||||||
attribute: 'StudyInstanceUID',
|
attribute: 'StudyInstanceUID',
|
||||||
constraint: {
|
constraint: {
|
||||||
equals: {
|
equals: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
|
||||||
value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
- `stages`: Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules.
|
- `stages`: Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules.
|
||||||
Therefore, the `stages` property is array of objects, each object being one stage.
|
Therefore, the `stages` property is array of objects, each object being one stage.
|
||||||
|
|
||||||
|
- `displaySets`: Defines the matching rules for which display sets to use.
|
||||||
- `viewportStructure`: Defines the layout of the viewer. You can define the
|
- `viewportStructure`: Defines the layout of the viewer. You can define the
|
||||||
number of `rows` and `columns`. There should be `rows * columns` number of
|
number of `rows` and `columns`.
|
||||||
viewport configuration in the `viewports` property. Note that order of
|
- `viewports` defines the actual viewports to display. There should be `rows * columns` number of
|
||||||
viewports are rows first then columns.
|
these `viewports` property, ordered rows first, then columns.
|
||||||
|
|
||||||
- `viewportSettings`: custom settings to be applied to the viewport. This can
|
|
||||||
be a `voi` being applied to the viewer or a tool to get enabled. We will
|
|
||||||
discuss viewport-specific settings [below](#viewport-settings)
|
|
||||||
|
|
||||||
- `imageMatchingRules (comming soon)`: setting the image slice for the
|
|
||||||
viewport.
|
|
||||||
|
|
||||||
- `seriesMatchingRules`: the most important rule that matches series in the
|
|
||||||
viewport. For instance, the following stage configuration will create a
|
|
||||||
one-by-two layout and put the series whose description contains `t2` on the
|
|
||||||
left, and a series with description that contains `adc` on the right. (order
|
|
||||||
of viewports are rows, first then columns)
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
id: 'hYbmMy3b7pz7GLiaT',
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
name: 'oneByThree',
|
name: 'oneByTwo',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
rows: 1,
|
rows: 1,
|
||||||
columns: 2,
|
columns: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
viewports: [
|
viewports: [
|
||||||
// viewport 1
|
// viewport 1
|
||||||
{
|
{
|
||||||
viewportSettings: [],
|
viewportOptions: {
|
||||||
imageMatchingRules: [],
|
viewportId: 'ctAXIAL',
|
||||||
seriesMatchingRules: [
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: 'ctToolGroup',
|
||||||
|
initialImageOptions: {
|
||||||
|
// index: 5,
|
||||||
|
preset: 'first', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
{
|
{
|
||||||
id: 'vSjk7NCYjtdS3XZAw',
|
type: 'cameraPosition',
|
||||||
weight: 1,
|
id: 'axialSync',
|
||||||
attribute: 'SeriesDescription',
|
source: true,
|
||||||
constraint: {
|
target: true,
|
||||||
contains: {
|
|
||||||
value: 't2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: false,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
studyMatchingRules: [],
|
|
||||||
},
|
},
|
||||||
// viewport 2
|
displaySets: [
|
||||||
{
|
{
|
||||||
viewportSettings: [],
|
id: 'displaySet',
|
||||||
imageMatchingRules: [],
|
|
||||||
seriesMatchingRules: [
|
|
||||||
{
|
|
||||||
id: 'vSjk7NCYjtdS3XZAw',
|
|
||||||
weight: 1,
|
|
||||||
attribute: 'SeriesDescription',
|
|
||||||
constraint: {
|
|
||||||
contains: {
|
|
||||||
value: 'ADC',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
studyMatchingRules: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -194,8 +227,9 @@ There are two events that get publish in `HangingProtocolService`:
|
|||||||
- `addProtocols`: adds provided protocols to the list of registered protocols
|
- `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, displaySets }, protocol)`: runs the HPService with the provided
|
||||||
studyMetaData and optional protocol. If protocol is not given, HP Matching
|
list of studies, display sets and optional protocol.
|
||||||
|
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.
|
||||||
|
|
||||||
@ -206,7 +240,11 @@ There are two events that get publish in `HangingProtocolService`:
|
|||||||
protocol definitions. `addCustomViewportSetting` is another way to set these
|
protocol definitions. `addCustomViewportSetting` is another way to set these
|
||||||
settings which is exposed by API
|
settings which is exposed by API
|
||||||
|
|
||||||
-
|
- `hps.applyCustomViewportSettings(viewportOptions, viewport,...args)` will run
|
||||||
|
the callback registered with addCustomViewportSetting for all custom settings
|
||||||
|
whose name matches the id of the custom viewport, with the arguments (id, value, viewport, ...args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Default initialization of the modes handles running the `HangingProtocolService`
|
Default initialization of the modes handles running the `HangingProtocolService`
|
||||||
|
|
||||||
@ -278,7 +316,7 @@ function modeFactory() {
|
|||||||
metaData => getFirstMeasurementSeriesInstanceUID(metaData)
|
metaData => getFirstMeasurementSeriesInstanceUID(metaData)
|
||||||
);
|
);
|
||||||
|
|
||||||
HangingProtocolService.run(studyMetadata);
|
HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets());
|
||||||
};
|
};
|
||||||
|
|
||||||
DicomMetadataStore.subscribe(
|
DicomMetadataStore.subscribe(
|
||||||
|
|||||||
@ -59,7 +59,7 @@ async function defaultRouteInit({
|
|||||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||||
({ StudyInstanceUID }) => {
|
({ StudyInstanceUID }) => {
|
||||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
HangingProtocolService.run(studyMetadata);
|
HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets());
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
unsubscriptions.push(seriesAddedUnsubscribe);
|
unsubscriptions.push(seriesAddedUnsubscribe);
|
||||||
|
|||||||
@ -26,12 +26,12 @@ function ViewerViewportGrid(props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const {
|
||||||
matchDetails,
|
viewportMatchDetails,
|
||||||
hpAlreadyApplied,
|
hpAlreadyApplied,
|
||||||
] = HangingProtocolService.getState();
|
} = HangingProtocolService.getMatchDetails();
|
||||||
|
|
||||||
if (!matchDetails.length) {
|
if (!viewportMatchDetails.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,24 +43,24 @@ function ViewerViewportGrid(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if current viewport doesn't have a match
|
// if current viewport doesn't have a match
|
||||||
if (matchDetails[i] === undefined) return;
|
if (viewportMatchDetails[i] === undefined) {
|
||||||
|
|
||||||
const { displaySetsInfo, viewportOptions } = matchDetails[i];
|
|
||||||
|
|
||||||
const displaySetUIDsToHang = [];
|
|
||||||
const displaySetUIDsToHangOptions = [];
|
|
||||||
displaySetsInfo.forEach(({ SeriesInstanceUID, displaySetOptions }) => {
|
|
||||||
const matchingDisplaySet = availableDisplaySets.find(ds => {
|
|
||||||
return ds.SeriesInstanceUID === SeriesInstanceUID;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!matchingDisplaySet) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
displaySetUIDsToHang.push(matchingDisplaySet.displaySetInstanceUID);
|
const { displaySetsInfo, viewportOptions } = viewportMatchDetails[i];
|
||||||
|
|
||||||
|
const displaySetUIDsToHang = [];
|
||||||
|
const displaySetUIDsToHangOptions = [];
|
||||||
|
displaySetsInfo.forEach(
|
||||||
|
({ displaySetInstanceUID, displaySetOptions }) => {
|
||||||
|
if (!displaySetInstanceUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySetUIDsToHang.push(displaySetInstanceUID);
|
||||||
displaySetUIDsToHangOptions.push(displaySetOptions);
|
displaySetUIDsToHangOptions.push(displaySetOptions);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!displaySetUIDsToHang.length) {
|
if (!displaySetUIDsToHang.length) {
|
||||||
continue;
|
continue;
|
||||||
@ -113,10 +113,11 @@ function ViewerViewportGrid(props) {
|
|||||||
|
|
||||||
// Using Hanging protocol engine to match the displaySets
|
// Using Hanging protocol engine to match the displaySets
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = DisplaySetService.subscribe(
|
const { unsubscribe } = HangingProtocolService.subscribe(
|
||||||
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
|
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||||
activeDisplaySets => {
|
() => {
|
||||||
updateDisplaySetsForViewports(activeDisplaySets);
|
const displaySets = DisplaySetService.getActiveDisplaySets();
|
||||||
|
updateDisplaySetsForViewports(displaySets);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -369,6 +370,11 @@ function _getViewportComponent(displaySets, viewportComponents) {
|
|||||||
const SOPClassHandlerId = displaySets[0].SOPClassHandlerId;
|
const SOPClassHandlerId = displaySets[0].SOPClassHandlerId;
|
||||||
|
|
||||||
for (let i = 0; i < viewportComponents.length; i++) {
|
for (let i = 0; i < viewportComponents.length; i++) {
|
||||||
|
if (!viewportComponents[i])
|
||||||
|
throw new Error('viewport components not defined');
|
||||||
|
if (!viewportComponents[i].displaySetsToDisplay) {
|
||||||
|
throw new Error('displaySetsToDisplay is null');
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)
|
viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)
|
||||||
) {
|
) {
|
||||||
@ -376,6 +382,7 @@ function _getViewportComponent(displaySets, viewportComponents) {
|
|||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
throw new Error(`No display set handler for ${SOPClassHandlerId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ViewerViewportGrid;
|
export default ViewerViewportGrid;
|
||||||
|
|||||||
@ -9,24 +9,26 @@ import { useQuery } from '@hooks';
|
|||||||
import ViewportGrid from '@components/ViewportGrid';
|
import ViewportGrid from '@components/ViewportGrid';
|
||||||
import Compose from './Compose';
|
import Compose from './Compose';
|
||||||
|
|
||||||
async function defaultRouteInit({
|
/**
|
||||||
servicesManager,
|
* Initialize the route.
|
||||||
studyInstanceUIDs,
|
*
|
||||||
dataSource,
|
* @param props.servicesManager to read services from
|
||||||
}) {
|
* @param props.studyInstanceUIDs for a list of studies to read
|
||||||
|
* @param props.dataSource to read the data from
|
||||||
|
* @returns array of subscriptions to cancel
|
||||||
|
*/
|
||||||
|
function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) {
|
||||||
const {
|
const {
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const unsubscriptions = [];
|
const unsubscriptions = [];
|
||||||
// TODO: This should be baked into core, not manual?
|
|
||||||
// DisplaySetService would wire this up?
|
|
||||||
const {
|
const {
|
||||||
unsubscribe: instanceAddedUnsubscribe,
|
unsubscribe: instanceAddedUnsubscribe,
|
||||||
} = DicomMetadataStore.subscribe(
|
} = DicomMetadataStore.subscribe(
|
||||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||||
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
|
function({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
|
||||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID
|
SeriesInstanceUID
|
||||||
@ -38,19 +40,43 @@ async function defaultRouteInit({
|
|||||||
|
|
||||||
unsubscriptions.push(instanceAddedUnsubscribe);
|
unsubscriptions.push(instanceAddedUnsubscribe);
|
||||||
|
|
||||||
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
|
||||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
dataSource.retrieve.series.metadata({ StudyInstanceUID })
|
||||||
({ StudyInstanceUID, madeInClient }) => {
|
|
||||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
|
||||||
if (!madeInClient) {
|
|
||||||
HangingProtocolService.run(studyMetadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
unsubscriptions.push(seriesAddedUnsubscribe);
|
|
||||||
|
|
||||||
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
// The hanging protocol matching service is fairly expensive to run multiple
|
||||||
dataSource.retrieve.series.metadata({ StudyInstanceUID });
|
// times, and doesn't allow partial matches to be made (it will simply fail
|
||||||
|
// to display anything if a required match fails), so we wait here until all metadata
|
||||||
|
// is retrieved (which will synchronously trigger the display set creation)
|
||||||
|
// until we run the hanging protocol matching service.
|
||||||
|
|
||||||
|
Promise.allSettled(allRetrieves).then(() => {
|
||||||
|
const displaySets = DisplaySetService.getActiveDisplaySets();
|
||||||
|
|
||||||
|
if (!displaySets || !displaySets.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const studyMap = {};
|
||||||
|
|
||||||
|
// Prior studies don't quite work properly yet, but the studies list
|
||||||
|
// is at least being generated and passed in.
|
||||||
|
const studies = displaySets.reduce((prev, curr) => {
|
||||||
|
const { StudyInstanceUID } = curr;
|
||||||
|
if (!studyMap[StudyInstanceUID]) {
|
||||||
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
|
studyMap[StudyInstanceUID] = study;
|
||||||
|
prev.push(study);
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// The assumption is that the display set at position 0 is the first
|
||||||
|
// 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 });
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscriptions;
|
return unsubscriptions;
|
||||||
@ -236,7 +262,7 @@ export default function ModeRoute({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await defaultRouteInit({
|
return defaultRouteInit({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
studyInstanceUIDs,
|
studyInstanceUIDs,
|
||||||
dataSource,
|
dataSource,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user