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:
Bill Wallace 2022-09-07 11:59:11 -04:00 committed by GitHub
parent 7c0756824e
commit e1d366e1e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 893 additions and 979 deletions

View File

@ -39,6 +39,8 @@ class CornerstoneViewportService implements IViewportService {
renderingEngine: Types.IRenderingEngine | null;
viewportsInfo: Map<number, ViewportInfo>;
viewportGridResizeObserver: ResizeObserver | null;
// TODO - get the right type here.
hangingProtocolService: object;
/**
* Service-specific
@ -59,7 +61,7 @@ class CornerstoneViewportService implements IViewportService {
this.listeners = {};
this.EVENTS = EVENTS;
const { HangingProtocolService } = servicesManager.services;
this.HangingProtocolService = HangingProtocolService;
this.hangingProtocolService = HangingProtocolService;
Object.assign(this, pubSubServiceInterface);
//
}
@ -166,8 +168,10 @@ class CornerstoneViewportService implements IViewportService {
const viewportInfo = this.viewportsInfo.get(viewportIndex);
viewportInfo.setRenderingEngineId(renderingEngine.id);
const { viewportOptions, displaySetOptions } =
this._getViewportAndDisplaySetOptions(
const {
viewportOptions,
displaySetOptions,
} = this._getViewportAndDisplaySetOptions(
publicViewportOptions,
publicDisplaySetOptions,
viewportInfo
@ -265,7 +269,6 @@ class CornerstoneViewportService implements IViewportService {
viewportInfo: ViewportInfo
) {
const displaySetOptions = viewportInfo.getDisplaySetOptions();
const { imageIds, initialImageIndex } = viewportData;
let initialImageIndexToUse = initialImageIndex;
@ -352,7 +355,7 @@ class CornerstoneViewportService implements IViewportService {
// (This call may or may not create sub-requests for series metadata)
const volumeInputArray = [];
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
const { HangingProtocolService } = this;
const { hangingProtocolService } = this;
for (let i = 0; i < viewportData.imageIds.length; i++) {
const imageIds = viewportData.imageIds[i];
@ -384,11 +387,11 @@ class CornerstoneViewportService implements IViewportService {
}
if (
HangingProtocolService.hasCustomImageLoadStrategy() &&
!HangingProtocolService.customImageLoadPerformed
hangingProtocolService.hasCustomImageLoadStrategy() &&
!hangingProtocolService.customImageLoadPerformed
) {
// 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,
volumeInputArray,
});
@ -415,8 +418,9 @@ class CornerstoneViewportService implements IViewportService {
) {
const { index, preset } = initialImageOptions;
const { numberOfSlices } =
csUtils.getImageSliceDataForVolumeViewport(viewport);
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
viewport
);
const imageIndex = this._getInitialImageIndex(
numberOfSlices,

View File

@ -16,7 +16,6 @@ export type ViewportOptions = {
viewportId: string;
orientation?: Types.Orientation;
background?: Types.Point3;
initialView?: string;
syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>;
@ -28,7 +27,6 @@ export type PublicViewportOptions = {
viewportId?: string;
orientation?: string;
background?: Types.Point3;
initialView?: string;
syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions;
customViewportProps?: Record<string, unknown>;

View File

@ -8,8 +8,8 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
/**
* This function caches the volumeUIDs until all the volumes inside the
* hangging protocol are initialized. Then it goes through the imageIds
* of the volumes, and interleav them, in order for the volumes to be loaded
* hanging protocol are initialized. Then it goes through the imageIds
* of the volumes, and interleave them, in order for the volumes to be loaded
* together from middle to the start and the end.
* @param {Object} props image loading properties from Cornerstone ViewportService
* @returns
@ -17,7 +17,7 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
export default function interleaveCenterLoader({
data: { viewportId, volumeInputArray },
displaySetsMatchDetails,
matchDetails,
viewportMatchDetails: matchDetails,
}) {
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);

View File

@ -16,7 +16,7 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
export default function interleaveTopToBottom({
data: { viewportId, volumeInputArray },
displaySetsMatchDetails,
matchDetails,
viewportMatchDetails: matchDetails,
}) {
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);

View File

@ -473,12 +473,13 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
const numberOfSeries = seriesPromises.length;
seriesPromises.forEach(async (seriesPromise, index) => {
const instances = await seriesPromise;
const seriesDeliveredPromises = seriesPromises.map(promise =>
promise.then(instances => {
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
});
})
);
await Promise.all(seriesDeliveredPromises);
setSuccessFlag();
},
deleteStudyMetadataPromise,
getImageIdsForDisplaySet(displaySet) {

View File

@ -23,7 +23,9 @@ const defaultProtocol = {
displaySets: [
{
id: 'displaySet',
// Unused currently
imageMatchingRules: [],
// Matches displaysets, NOT series
seriesMatchingRules: [],
studyMatchingRules: [],
},

View File

@ -28,12 +28,12 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
DisplaySetService,
ToolGroupService,
ToolBarService,
HangingProtocolService,
} = servicesManager.services;
const [metadata, setMetadata] = useState(DEFAULT_MEATADATA);
const [ptDisplaySet, setPtDisplaySet] = useState(null);
const handleMetadataChange = useCallback(
metadata => {
const handleMetadataChange = metadata => {
setMetadata(prevState => {
const newState = { ...prevState };
Object.keys(metadata).forEach(key => {
@ -48,12 +48,12 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
});
return newState;
});
},
[metadata]
);
};
const getMatchingPTDisplaySet = useCallback(() => {
const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet');
const getMatchingPTDisplaySet = viewportMatchDetails => {
const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet', {
viewportMatchDetails,
});
if (!ptDisplaySet) {
return;
@ -67,16 +67,16 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
ptDisplaySet,
metadata,
};
}, []);
};
useEffect(() => {
const displaySets = DisplaySetService.activeDisplaySets;
const displaySets = DisplaySetService.getActiveDisplaySets();
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
if (!displaySets.length) {
return;
}
const displaySetInfo = getMatchingPTDisplaySet();
const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails);
if (!displaySetInfo) {
return;
@ -89,15 +89,14 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
// get the patientMetadata from the StudyInstanceUIDs and update the state
useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
() => {
const displaySetInfo = getMatchingPTDisplaySet();
const { unsubscribe } = HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
({ viewportMatchDetails }) => {
const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails);
if (!displaySetInfo) {
return;
}
const { ptDisplaySet, metadata } = displaySetInfo;
setPtDisplaySet(ptDisplaySet);
setMetadata(metadata);

View File

@ -41,9 +41,9 @@ const commandsModule = ({
}
function _getMatchedViewportsToolGroupIds() {
const [matchedViewports] = HangingProtocolService.getState();
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
const toolGroupIds = [];
matchedViewports.forEach(({ viewportOptions }) => {
viewportMatchDetails.forEach(({ viewportOptions }) => {
const { toolGroupId } = viewportOptions;
if (toolGroupIds.indexOf(toolGroupId) === -1) {
toolGroupIds.push(toolGroupId);
@ -54,33 +54,30 @@ const commandsModule = ({
}
const actions = {
getMatchingPTDisplaySet: () => {
getMatchingPTDisplaySet: ({ viewportMatchDetails }) => {
// 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
// that are in the viewer layout (but then we have the problem of the attenuation
// corrected PT vs the non-attenuation correct PT)
const matches = HangingProtocolService.getDisplaySetsMatchDetails();
const matchedSeriesInstanceUIDs = Array.from(matches.values()).map(
({ SeriesInstanceUID }) => SeriesInstanceUID
);
let ptDisplaySet = null;
for (const SeriesInstanceUID of matchedSeriesInstanceUIDs) {
const displaySets = DisplaySetService.getDisplaySetsForSeries(
SeriesInstanceUID
for (const matched of viewportMatchDetails) {
const { displaySetsInfo } = matched;
const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) =>
DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
);
if (!displaySets || displaySets.length === 0) {
continue;
}
const displaySet = displaySets[0];
if (displaySet.Modality !== 'PT') {
continue;
}
ptDisplaySet = displaySets.find(
displaySet => displaySet.Modality === 'PT'
);
ptDisplaySet = displaySet;
if (ptDisplaySet) {
break;
}
}
return ptDisplaySet;
@ -121,7 +118,10 @@ const commandsModule = ({
createNewLabelmapFromPT: async () => {
// Create a segmentation of the same resolution as the source data
// using volumeLoader.createAndCacheDerivedVolume.
const ptDisplaySet = actions.getMatchingPTDisplaySet();
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
const ptDisplaySet = actions.getMatchingPTDisplaySet({
viewportMatchDetails,
});
if (!ptDisplaySet) {
UINotificationService.error('No matching PT display set found');
@ -565,8 +565,11 @@ const commandsModule = ({
},
setFusionPTColormap: ({ toolGroupId, colormap }) => {
const toolGroup = ToolGroupService.getToolGroup(toolGroupId);
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
const ptDisplaySet = actions.getMatchingPTDisplaySet();
const ptDisplaySet = actions.getMatchingPTDisplaySet({
viewportMatchDetails,
});
if (!ptDisplaySet) {
return;

View File

@ -6,10 +6,6 @@
module.exports = {
verbose: true,
roots: ['<rootDir>/src'],
transform: {
'^.+\\.js$': 'babel-jest',
'^.+\\.jsx$': 'babel-jest',
},
testMatch: ['<rootDir>/src/**/*.test.js'],
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'],

View File

@ -100,17 +100,19 @@ function modeFactory({ modeConfiguration }) {
// 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
// when slabThickness is modified
const matches = HangingProtocolService.getDisplaySetsMatchDetails();
const {
displaySetMatchDetails,
} = HangingProtocolService.getMatchDetails();
setCrosshairsConfiguration(
matches,
displaySetMatchDetails,
toolNames,
ToolGroupService,
DisplaySetService
);
setFusionActiveVolume(
matches,
displaySetMatchDetails,
toolNames,
ToolGroupService,
DisplaySetService

View File

@ -211,7 +211,14 @@ const BaseImplementation = {
let study = _getStudy(StudyInstanceUID);
if (!study) {
study = createStudyMetadata(StudyInstanceUID);
// Will typically be undefined with a compliant DICOMweb server, reset later
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);
}

View File

@ -0,0 +1,7 @@
/** Defines a typescript type for study metadata */
interface StudyMetadata {
readonly StudyInstanceUID: string;
StudyDescription?: string;
}
export default StudyMetadata;

View File

@ -4,6 +4,7 @@ function createStudyMetadata(StudyInstanceUID) {
return {
StudyInstanceUID,
StudyDescription: '',
ModalitiesInStudy: [],
isLoaded: false,
series: [],
/**
@ -13,6 +14,9 @@ function createStudyMetadata(StudyInstanceUID) {
*/
addInstanceToSeries: function(instance) {
const { SeriesInstanceUID } = instance;
if (!this.StudyDescription) {
this.StudyDescription = instance.StudyDescription;
}
const existingSeries = this.series.find(
s => s.SeriesInstanceUID === SeriesInstanceUID
);
@ -22,16 +26,24 @@ function createStudyMetadata(StudyInstanceUID) {
} else {
const series = createSeriesMetadata([instance]);
this.series.push(series);
const { Modality } = series;
if (this.ModalitiesInStudy.indexof(Modality) === -1) {
this.ModalitiesInStudy.push(Modality);
}
}
},
/**
*
* @param {object[]} instances
* @param {string} instances[].SeriesInstanceUID
* @param {string} instances[].StudyDescription
* @returns {bool} true if series were added; false if series already exist
*/
addInstancesToSeries: function(instances) {
const { SeriesInstanceUID } = instances[0];
if (!this.StudyDescription) {
this.StudyDescription = instances[0].StudyDescription;
}
const existingSeries = this.series.find(
s => s.SeriesInstanceUID === SeriesInstanceUID
);

View File

@ -0,0 +1,7 @@
interface IDisplaySet {
displaySetInstanceUID: string;
StudyInstanceUID: string;
SeriesInstanceUID?: string;
}
export default IDisplaySet;

View File

@ -4,10 +4,18 @@ import validate from './lib/validator';
* Match a Metadata instance against rules using Validate.js for validation.
* @param {InstanceMetadata} metadataInstance Metadata instance object
* @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)
*/
const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
const options = {
const match = (
metadataInstance,
rules,
customAttributeRetrievalCallbacks,
options
) => {
const validateOptions = {
format: 'grouped',
};
@ -16,18 +24,32 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
failed: [],
};
const readValues = {};
let requiredFailed = false;
let score = 0;
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
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
const customAttribute = customAttributeRetrievalCallbacks[attribute];
metadataInstance[attribute] = customAttribute.callback(metadataInstance);
readValues[attribute] = customAttributeRetrievalCallbacks[
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
const testConstraint = {
[attribute]: rule.constraint,
@ -35,13 +57,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
// Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
let attributeValue = metadataInstance[attribute];
if (attributeValue === undefined) {
if (attribute === 'NumberOfStudyRelatedSeries') {
attributeValue = metadataInstance.series?.length;
}
// Add other computable values such as modalities in study
}
let attributeValue = readValues[attribute];
const attributeMap = {
[attribute]: attributeValue,
};
@ -49,7 +65,7 @@ const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
// Use Validate.js to evaluate the constraints on the specified metadataInstance
let errorMessages;
try {
errorMessages = validate(attributeMap, testConstraint, [options]);
errorMessages = validate(attributeMap, testConstraint, [validateOptions]);
} catch (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.
// 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
details.passed.push({
rule,

View File

@ -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: {},
},
],
});
});
});
});

View File

@ -1,53 +1,110 @@
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy.js';
import sortBy from '../../utils/sortBy';
import ProtocolEngine from './ProtocolEngine';
import StudyMetadata from '../DicomMetadataStore/StudyMetadata';
import IDisplaySet from '../DisplaySetService/IDisplaySet';
const EVENTS = {
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
PROTOCOL_CHANGED: 'event::hanging_protocol_changed',
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
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 {
constructor(commandsManager) {
this._commandsManager = commandsManager;
this.protocols = [];
this.ProtocolEngine = undefined;
this.protocol = undefined;
this.stage = undefined;
studies: StudyMetadata[];
protocols: Record<string, unknown>[];
protocol: Record<string, unknown>;
stage: number;
_commandsManager: Record<string, unknown>;
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
* hanging protocol, an object of the form
*
* {
* viewportOptions,
* displaySetsInfo, // contains array of [ { SeriesInstanceUID, displaySetOPtions}, ... ]
* }
*/
this.matchDetails = [];
/**
* displaySetMatchDetails = <displaySetId, match>
* DisplaySetId is the id defined in the hangingProtocol
* match is an object that contains information about
*
* {
* SeriesInstanceUID,
* StudyInstanceUID,
* matchDetails,
* matchingScore,
* sortingInfo
* }
*/
this.displaySetMatchDetails = new Map();
this.hpAlreadyApplied = [];
viewportMatchDetails = [] as ViewportMatchDetails[];
constructor(commandsManager) {
this._commandsManager = commandsManager;
this.protocols = [];
this.protocolEngine = undefined;
this.protocol = undefined;
this.stage = undefined;
this.studies = [];
this.customViewportSettings = [];
this.customAttributeRetrievalCallbacks = {};
this.listeners = {};
this.registeredImageLoadStrategies = {};
this.activeImageLoadStrategyName = null;
this.customImageLoadPerformed = false;
Object.defineProperty(this, 'EVENTS', {
value: EVENTS,
writable: false,
@ -57,27 +114,27 @@ class HangingProtocolService {
Object.assign(this, pubSubServiceInterface);
}
reset() {
public reset() {
this.studies = [];
this.protocols = [];
this.hpAlreadyApplied = [];
this.matchDetails = [];
this.viewportMatchDetails = [];
// this.ProtocolEngine.reset()
}
getDisplaySetsMatchDetails() {
return this.displaySetMatchDetails;
public getMatchDetails(): HangingProtocolMatchDetails {
return {
viewportMatchDetails: this.viewportMatchDetails,
displaySetMatchDetails: this.displaySetMatchDetails,
hpAlreadyApplied: this.hpAlreadyApplied,
};
}
getState() {
return [this.matchDetails, this.hpAlreadyApplied];
}
getProtocols() {
public getProtocols() {
return this.protocols;
}
addProtocols(protocols) {
public addProtocols(protocols) {
protocols.forEach(protocol => {
if (this.protocols.indexOf(protocol) === -1) {
this.protocols.push(this._validateProtocol(protocol));
@ -85,21 +142,37 @@ class HangingProtocolService {
});
}
run(studyMetaData, protocol) {
if (!this.studies.includes(studyMetaData)) {
this.studies.push(studyMetaData);
}
// copy here so we don't mutate it
const metaData = Object.assign({}, studyMetaData);
/**
* Run the hanging protocol decisions tree on the active study,
* studies list and display sets, firing a hanging protocol event when
* complete to indicate the hanging protocol is ready.
*
* @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.customAttributeRetrievalCallbacks
);
// if there is no pre-defined protocol
if (!protocol || protocol.id === undefined) {
const matchedProtocol = this.ProtocolEngine.run(metaData);
const matchedProtocol = this.protocolEngine.run({
studies: this.studies,
activeStudy,
displaySets,
});
this._setProtocol(matchedProtocol);
return;
}
@ -112,7 +185,7 @@ class HangingProtocolService {
* and its callback has been added to the HangingProtocolService
* @returns {boolean} true
*/
hasCustomImageLoadStrategy() {
public hasCustomImageLoadStrategy() {
return (
this.activeImageLoadStrategyName !== null &&
this.registeredImageLoadStrategies[
@ -121,7 +194,7 @@ class HangingProtocolService {
);
}
getCustomImageLoadPerformed() {
public getCustomImageLoadPerformed() {
return this.customImageLoadPerformed;
}
@ -130,13 +203,13 @@ class HangingProtocolService {
* @param {string} name strategy name
* @param {Function} callback image loader callback
*/
registerImageLoadStrategy(name, callback) {
public registerImageLoadStrategy(name, callback) {
if (callback instanceof Function && name) {
this.registeredImageLoadStrategies[name] = callback;
}
}
setHangingProtocolAppliedForViewport(i) {
public setHangingProtocolAppliedForViewport(i) {
this.hpAlreadyApplied[i] = true;
}
@ -147,18 +220,21 @@ class HangingProtocolService {
* @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 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] = {
...options,
id: attributeId,
name: attributeName,
callback: callback,
callback,
};
}
/**
* Switches to the next protocol stage in the display set sequence
*/
nextProtocolStage() {
public nextProtocolStage() {
console.log('ProtocolEngine::nextProtocolStage');
if (!this._setCurrentProtocolStage(1)) {
@ -169,7 +245,7 @@ class HangingProtocolService {
/**
* Switches to the previous protocol stage in the display set sequence
*/
previousProtocolStage() {
public previousProtocolStage() {
console.log('ProtocolEngine::previousProtocolStage');
if (!this._setCurrentProtocolStage(-1)) {
@ -187,8 +263,8 @@ class HangingProtocolService {
];
const loadedData = loader({
data,
displaySetsMatchDetails: this.getDisplaySetsMatchDetails(),
matchDetails: this.matchDetails,
displaySetsMatchDetails: this.displaySetMatchDetails,
viewportMatchDetails: this.viewportMatchDetails,
});
// if loader successfully re-arranged the data with the custom strategy
@ -253,6 +329,12 @@ class HangingProtocolService {
}
}
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];
}
/**
* Updates the viewports with the selected protocol stage.
*/
_updateViewports() {
// Make sure we have an active protocol with a non-empty array of display sets
if (!this._getNumProtocolStages()) {
console.log('No protocol stages - nothing to display');
return;
}
@ -301,6 +387,7 @@ class HangingProtocolService {
!stageModel.displaySets ||
!stageModel.viewports.length
) {
console.log('Stage cannot be applied', stageModel);
return;
}
@ -311,6 +398,7 @@ class HangingProtocolService {
// If no such layout properties exist, stop here.
const layoutProps = stageModel.viewportStructure.properties;
if (!layoutProps) {
console.log('No viewportStructure.properties in', stageModel);
return;
}
@ -324,12 +412,13 @@ class HangingProtocolService {
});
// 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 => {
const { bestMatch } = this._matchImages(displaySet);
const { bestMatch, matchingScores } = this._matchImages(displaySet);
this.displaySetMatchDetails.set(displaySet.id, bestMatch);
if (bestMatch) {
bestMatch.matchingScores = matchingScores;
}
});
// Loop through each viewport
@ -339,15 +428,26 @@ class HangingProtocolService {
// DisplaySets for the viewport, Note: this is not the actual displaySet,
// but it is a info to locate the displaySet from the displaySetService
let displaySetsInfo = [];
viewport.displaySets.forEach(({ id, options: displaySetOptions }) => {
const viewportDisplaySet = this.displaySetMatchDetails.get(id);
const displaySetsInfo = [];
viewport.displaySets.forEach(
({ 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) {
const { SeriesInstanceUID } = viewportDisplaySet;
const {
SeriesInstanceUID,
displaySetInstanceUID,
} = viewportDisplaySet;
const displaySetInfo = {
SeriesInstanceUID,
displaySetInstanceUID,
displaySetOptions,
};
@ -360,9 +460,10 @@ class HangingProtocolService {
`
);
}
});
}
);
this.matchDetails[viewportIndex] = {
this.viewportMatchDetails[viewportIndex] = {
viewportOptions,
displaySetsInfo,
};
@ -370,50 +471,77 @@ class HangingProtocolService {
}
// Match images given a list of Studies and a Viewport's image matching reqs
_matchImages(displaySet) {
console.log('ProtocolEngine::matchImages');
_matchImages(displaySetRules) {
// TODO: matching is applied on study and series level, instance
// level matching needs to be added in future
// 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 = [];
let highestStudyMatchingScore = 0;
let highestSeriesMatchingScore = 0;
console.log(
'ProtocolEngine::matchImages',
studyMatchingRules,
seriesMatchingRules
);
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,
studyMatchingRules
studyMatchingRules,
{ studies: this.studies, displaySets: studyDisplaySets }
);
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
if (
studyMatchDetails.requiredFailed === true ||
studyMatchDetails.score < highestStudyMatchingScore
) {
if (studyMatchDetails.requiredFailed === true) {
return;
}
highestStudyMatchingScore = studyMatchDetails.score;
study.series.forEach(aSeries => {
const seriesMatchDetails = this.ProtocolEngine.findMatch(
aSeries,
seriesMatchingRules
this.debug(
'study',
study.StudyInstanceUID,
'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
if (
seriesMatchDetails.requiredFailed === true ||
seriesMatchDetails.score < highestSeriesMatchingScore
) {
if (seriesMatchDetails.requiredFailed === true) {
this.debug(
'Display set required failed',
displaySet,
seriesMatchingRules
);
return;
}
highestSeriesMatchingScore = seriesMatchDetails.score;
this.debug('Found displaySet for rules', displaySet);
highestSeriesMatchingScore = Math.max(
seriesMatchDetails.score,
highestSeriesMatchingScore
);
const matchDetails = {
passed: [],
@ -438,21 +566,27 @@ class HangingProtocolService {
seriesMatchDetails.score + studyMatchDetails.score;
const imageDetails = {
StudyInstanceUID: study.StudyInstanceUID,
SeriesInstanceUID: aSeries.SeriesInstanceUID,
StudyInstanceUID,
SeriesInstanceUID,
displaySetInstanceUID,
matchingScore: totalMatchScore,
matchDetails: matchDetails,
sortingInfo: {
score: totalMatchScore,
study: study.StudyInstanceUID,
series: parseInt(aSeries.SeriesNumber),
series: parseInt(displaySet.SeriesNumber),
},
};
this.debug('Adding display set', displaySet, imageDetails);
matchingScores.push(imageDetails);
});
});
if (matchingScores.length === 0) {
console.log('No match found');
}
// Sort the matchingScores
const sortingFunction = sortBy(
{
@ -473,7 +607,11 @@ class HangingProtocolService {
const bestMatch = matchingScores[0];
console.log('ProtocolEngine::matchImages bestMatch', bestMatch);
console.log(
'ProtocolEngine::matchImages bestMatch',
bestMatch,
matchingScores
);
return {
bestMatch,
@ -495,7 +633,7 @@ class HangingProtocolService {
* Check if the previous stage is available
* @return {Boolean} True if previous stage is available or false otherwise
*/
_isPreviousStageAvailable() {
_isPreviousStageAvailable(): boolean {
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
* @return {Boolean} True if new stage has set or false, otherwise
*/
_setCurrentProtocolStage(stageAction) {
_setCurrentProtocolStage(stageAction): boolean {
//reseting the applied protocols
this.hpAlreadyApplied = [];
// Check if previous or next stage is available
@ -520,9 +658,7 @@ class HangingProtocolService {
this.stage += stageAction;
// Log the new stage
console.log(
`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`
);
this.debug(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
// Since stage has changed, we need to update the viewports
// and redo matchings
@ -530,11 +666,22 @@ class HangingProtocolService {
// Everything went well
this._broadcastChange(this.EVENTS.STAGE_CHANGE, {
matchDetails: this.matchDetails,
viewportMatchDetails: this.viewportMatchDetails,
hpAlreadyApplied: this.hpAlreadyApplied,
displaySetMatchDetails: this.displaySetMatchDetails,
});
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.
*

View File

@ -10,8 +10,15 @@ export default class ProtocolEngine {
this.study = undefined;
}
run(studyMetaData) {
this.study = studyMetaData;
/** Evaluate the hanging protocol matches on the given:
* @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();
}
@ -49,9 +56,11 @@ export default class ProtocolEngine {
// Clear all data currently in matchedProtocols
this._clearMatchedProtocols();
// TODO: handle more than one study
const study = this.study;
const matched = this.findMatchByStudy(study);
// TODO: handle more than one study - this.studies has the list of studies
const matched = this.findMatchByStudy(this.study, {
studies: this.studies,
displaySets: this.displaySets,
});
// For each matched protocol, check if it is already in MatchedProtocols
matched.forEach(matchedDetail => {
@ -72,11 +81,12 @@ export default class ProtocolEngine {
});
}
findMatch(metaData, rules) {
findMatch(metaData, rules, options) {
return HPMatcher.match(
metaData,
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
* with the given study. The best protocol are ordered by score and returned in an array
* @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
* Each match object has the score of the matching and the matched
* protocol
*/
findMatchByStudy(study) {
findMatchByStudy(study, options) {
const matched = [];
this.protocols.forEach(protocol => {
@ -98,13 +109,14 @@ export default class ProtocolEngine {
let rules = protocol.protocolMatchingRules.slice();
if (!rules || !rules.length) {
console.warn(
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules'
'ProtocolEngine::findMatchByStudy no matching rules - specify protocolMatchingRules',
protocol.id
);
return;
}
// Run the matcher and get matching details
const matchedDetails = this.findMatch(study, rules);
const matchedDetails = this.findMatch(study, rules, options);
const score = matchedDetails.score;
// The protocol matched some rule, add it to the matched list
@ -156,605 +168,3 @@ export default class ProtocolEngine {
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');
// }
// }
// }

View File

@ -1,39 +1,88 @@
import validate from 'validate.js';
validate.validators.equals = function (value, options, key, attributes) {
if (options && value !== options.value) {
return key + 'must equal ' + options.value;
validate.validators.equals = function(value, options, key, attributes) {
const testValue = options?.value ?? options;
if (value !== testValue) {
return key + 'must equal ' + testValue;
}
};
validate.validators.doesNotEqual = function (value, options, key) {
if (options && value === options.value) {
return key + 'cannot equal ' + options.value;
validate.validators.doesNotEqual = function(value, options, key) {
const testValue = options?.value ?? options;
if (value === testValue) {
return key + 'cannot equal ' + testValue;
}
};
validate.validators.contains = function (value, options, key) {
if (options && value.indexOf && value.indexOf(options.value) === -1) {
return key + 'must contain ' + options.value;
validate.validators.contains = function(value, options, key) {
const testValue = options?.value ?? options;
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) {
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)) {
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)) {
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;

View File

@ -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();
})
})
});

View File

@ -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.
```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 <> /** ... **/ </>
}
```
DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md).
## Events
There are three events that get broadcasted in `DisplaySetService`:

View File

@ -59,7 +59,6 @@ There are three events that get broadcasted in `DisplaySetService`:
## API
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
- `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.

View File

@ -29,7 +29,7 @@ You can find the skeleton of the hanging protocols here:
```js
const defaultProtocol = {
id: 'defaultProtocol',
id: 'test',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'Default',
@ -37,31 +37,91 @@ const defaultProtocol = {
modifiedDate: '2021-02-23T19:22:08.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
toolGroupIds: [
'ctToolGroup',
'ptToolGroup',
],
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
protocolMatchingRules: [
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'StudyDescription',
constraint: {
contains: {
value: 'PETCT',
},
},
required: false,
},
],
stages: [
{
id: 'nwzau7jDkEkL8djfr',
name: 'oneByOne',
id: 'hYbmMy3b7pz7GLiaT',
name: 'default',
viewportStructure: {
type: 'grid',
layoutType: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
displaySets: [
{
viewportSettings: [],
imageMatchingRules: [],
seriesMatchingRules: [],
id: 'displaySet',
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'Modality',
constraint: {
equals: 'CT',
},
required: true,
},
{
id: 'vSjk7NCYjtdS3XZAw',
weight: 1,
attribute: 'numImageFrames',
constraint: {
greaterThan: 10,
},
},
],
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,
};
}
```
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
protocols get sorted based on the weights, and the winning protocol gets
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.
[Learn more about custom attribute matching](#custom-attribute)
@ -85,89 +145,62 @@ Let's discuss each property in depth.
```js
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'StudyInstanceUID',
constraint: {
equals: {
value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
},
equals: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
},
required: true,
}
```
- `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.
- `displaySets`: Defines the matching rules for which display sets to use.
- `viewportStructure`: Defines the layout of the viewer. You can define the
number of `rows` and `columns`. There should be `rows * columns` number of
viewport configuration in the `viewports` property. Note that order of
viewports are rows first then columns.
number of `rows` and `columns`.
- `viewports` defines the actual viewports to display. There should be `rows * columns` number of
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
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
name: 'oneByThree',
name: 'oneByTwo',
viewportStructure: {
type: 'grid',
properties: {
rows: 1,
columns: 2,
columns: 3,
},
},
viewports: [
// viewport 1
{
viewportSettings: [],
imageMatchingRules: [],
seriesMatchingRules: [
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
orientation: 'axial',
toolGroupId: 'ctToolGroup',
initialImageOptions: {
// index: 5,
preset: 'first', // 'first', 'last', 'middle'
},
syncGroups: [
{
id: 'vSjk7NCYjtdS3XZAw',
weight: 1,
attribute: 'SeriesDescription',
constraint: {
contains: {
value: 't2',
},
},
required: false,
type: 'cameraPosition',
id: 'axialSync',
source: true,
target: true,
},
],
studyMatchingRules: [],
},
// viewport 2
displaySets: [
{
viewportSettings: [],
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'vSjk7NCYjtdS3XZAw',
weight: 1,
attribute: 'SeriesDescription',
constraint: {
contains: {
value: 'ADC',
},
},
required: true,
},
],
studyMatchingRules: [],
id: 'displaySet',
},
],
},
@ -194,8 +227,9 @@ There are two events that get publish in `HangingProtocolService`:
- `addProtocols`: adds provided protocols to the list of registered protocols
for matching
- `run(studyMetaData, protocol)`: runs the HPService with the provided
studyMetaData and optional protocol. If protocol is not given, HP Matching
- `run({ studies, displaySets }, protocol)`: runs the HPService with the provided
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
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
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`
@ -278,7 +316,7 @@ function modeFactory() {
metaData => getFirstMeasurementSeriesInstanceUID(metaData)
);
HangingProtocolService.run(studyMetadata);
HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets());
};
DicomMetadataStore.subscribe(

View File

@ -59,7 +59,7 @@ async function defaultRouteInit({
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
HangingProtocolService.run(studyMetadata);
HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets());
}
);
unsubscriptions.push(seriesAddedUnsubscribe);

View File

@ -26,12 +26,12 @@ function ViewerViewportGrid(props) {
return;
}
const [
matchDetails,
const {
viewportMatchDetails,
hpAlreadyApplied,
] = HangingProtocolService.getState();
} = HangingProtocolService.getMatchDetails();
if (!matchDetails.length) {
if (!viewportMatchDetails.length) {
return;
}
@ -43,24 +43,24 @@ function ViewerViewportGrid(props) {
}
// if current viewport doesn't have a match
if (matchDetails[i] === undefined) return;
const { displaySetsInfo, viewportOptions } = matchDetails[i];
const displaySetUIDsToHang = [];
const displaySetUIDsToHangOptions = [];
displaySetsInfo.forEach(({ SeriesInstanceUID, displaySetOptions }) => {
const matchingDisplaySet = availableDisplaySets.find(ds => {
return ds.SeriesInstanceUID === SeriesInstanceUID;
});
if (!matchingDisplaySet) {
if (viewportMatchDetails[i] === undefined) {
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);
});
}
);
if (!displaySetUIDsToHang.length) {
continue;
@ -113,10 +113,11 @@ function ViewerViewportGrid(props) {
// Using Hanging protocol engine to match the displaySets
useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
activeDisplaySets => {
updateDisplaySetsForViewports(activeDisplaySets);
const { unsubscribe } = HangingProtocolService.subscribe(
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
() => {
const displaySets = DisplaySetService.getActiveDisplaySets();
updateDisplaySetsForViewports(displaySets);
}
);
@ -369,6 +370,11 @@ function _getViewportComponent(displaySets, viewportComponents) {
const SOPClassHandlerId = displaySets[0].SOPClassHandlerId;
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 (
viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId)
) {
@ -376,6 +382,7 @@ function _getViewportComponent(displaySets, viewportComponents) {
return component;
}
}
throw new Error(`No display set handler for ${SOPClassHandlerId}`);
}
export default ViewerViewportGrid;

View File

@ -9,24 +9,26 @@ import { useQuery } from '@hooks';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
async function defaultRouteInit({
servicesManager,
studyInstanceUIDs,
dataSource,
}) {
/**
* Initialize the route.
*
* @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 {
DisplaySetService,
HangingProtocolService,
} = servicesManager.services;
const unsubscriptions = [];
// TODO: This should be baked into core, not manual?
// DisplaySetService would wire this up?
const {
unsubscribe: instanceAddedUnsubscribe,
} = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
function({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
@ -38,19 +40,43 @@ async function defaultRouteInit({
unsubscriptions.push(instanceAddedUnsubscribe);
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID, madeInClient }) => {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
if (!madeInClient) {
HangingProtocolService.run(studyMetadata);
}
}
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
dataSource.retrieve.series.metadata({ StudyInstanceUID })
);
unsubscriptions.push(seriesAddedUnsubscribe);
studyInstanceUIDs.forEach(StudyInstanceUID => {
dataSource.retrieve.series.metadata({ StudyInstanceUID });
// The hanging protocol matching service is fairly expensive to run multiple
// 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;
@ -236,7 +262,7 @@ export default function ModeRoute({
});
}
return await defaultRouteInit({
return defaultRouteInit({
servicesManager,
studyInstanceUIDs,
dataSource,