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;
|
||||
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,12 +168,14 @@ class CornerstoneViewportService implements IViewportService {
|
||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
||||
|
||||
const { viewportOptions, displaySetOptions } =
|
||||
this._getViewportAndDisplaySetOptions(
|
||||
publicViewportOptions,
|
||||
publicDisplaySetOptions,
|
||||
viewportInfo
|
||||
);
|
||||
const {
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
} = this._getViewportAndDisplaySetOptions(
|
||||
publicViewportOptions,
|
||||
publicDisplaySetOptions,
|
||||
viewportInfo
|
||||
);
|
||||
|
||||
viewportInfo.setViewportOptions(viewportOptions);
|
||||
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
storeInstances(instances);
|
||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||
});
|
||||
const seriesDeliveredPromises = seriesPromises.map(promise =>
|
||||
promise.then(instances => {
|
||||
storeInstances(instances);
|
||||
})
|
||||
);
|
||||
await Promise.all(seriesDeliveredPromises);
|
||||
setSuccessFlag();
|
||||
},
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet(displaySet) {
|
||||
|
||||
@ -23,7 +23,9 @@ const defaultProtocol = {
|
||||
displaySets: [
|
||||
{
|
||||
id: 'displaySet',
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
|
||||
@ -28,32 +28,32 @@ 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 => {
|
||||
setMetadata(prevState => {
|
||||
const newState = { ...prevState };
|
||||
Object.keys(metadata).forEach(key => {
|
||||
if (typeof metadata[key] === 'object') {
|
||||
newState[key] = {
|
||||
...prevState[key],
|
||||
...metadata[key],
|
||||
};
|
||||
} else {
|
||||
newState[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newState;
|
||||
const handleMetadataChange = metadata => {
|
||||
setMetadata(prevState => {
|
||||
const newState = { ...prevState };
|
||||
Object.keys(metadata).forEach(key => {
|
||||
if (typeof metadata[key] === 'object') {
|
||||
newState[key] = {
|
||||
...prevState[key],
|
||||
...metadata[key],
|
||||
};
|
||||
} else {
|
||||
newState[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
[metadata]
|
||||
);
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
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
|
||||
);
|
||||
|
||||
@ -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.
|
||||
* @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,
|
||||
|
||||
@ -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 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 {
|
||||
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
|
||||
*/
|
||||
viewportMatchDetails = [] as ViewportMatchDetails[];
|
||||
|
||||
constructor(commandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
this.protocols = [];
|
||||
this.ProtocolEngine = undefined;
|
||||
this.protocolEngine = undefined;
|
||||
this.protocol = undefined;
|
||||
this.stage = undefined;
|
||||
/**
|
||||
* 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 = [];
|
||||
|
||||
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,30 +428,42 @@ 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;
|
||||
if (viewportDisplaySet) {
|
||||
const {
|
||||
SeriesInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
} = viewportDisplaySet;
|
||||
|
||||
const displaySetInfo = {
|
||||
SeriesInstanceUID,
|
||||
displaySetOptions,
|
||||
};
|
||||
const displaySetInfo = {
|
||||
SeriesInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
displaySetOptions,
|
||||
};
|
||||
|
||||
displaySetsInfo.push(displaySetInfo);
|
||||
} else {
|
||||
console.warn(
|
||||
`
|
||||
displaySetsInfo.push(displaySetInfo);
|
||||
} else {
|
||||
console.warn(
|
||||
`
|
||||
The hanging protocol viewport is requesting to display ${id} displaySet that is not
|
||||
matched based on the provided criteria (e.g. matching rules).
|
||||
`
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
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.
|
||||
*
|
||||
@ -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');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
```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`:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,92 +145,65 @@ 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
|
||||
{
|
||||
viewportSettings: [],
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'vSjk7NCYjtdS3XZAw',
|
||||
weight: 1,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
contains: {
|
||||
value: 'ADC',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
if (viewportMatchDetails[i] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { displaySetsInfo, viewportOptions } = matchDetails[i];
|
||||
const { displaySetsInfo, viewportOptions } = viewportMatchDetails[i];
|
||||
|
||||
const displaySetUIDsToHang = [];
|
||||
const displaySetUIDsToHangOptions = [];
|
||||
displaySetsInfo.forEach(({ SeriesInstanceUID, displaySetOptions }) => {
|
||||
const matchingDisplaySet = availableDisplaySets.find(ds => {
|
||||
return ds.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
displaySetsInfo.forEach(
|
||||
({ displaySetInstanceUID, displaySetOptions }) => {
|
||||
if (!displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matchingDisplaySet) {
|
||||
return;
|
||||
displaySetUIDsToHang.push(displaySetInstanceUID);
|
||||
displaySetUIDsToHangOptions.push(displaySetOptions);
|
||||
}
|
||||
|
||||
displaySetUIDsToHang.push(matchingDisplaySet.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;
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user