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:
1 parent
7c0756824e
commit
e1d366e1e1
26 files changed
+893
-979
No files matched your search
@@ -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: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+257
-110
@@ -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();
|
||||
})
|
||||
})
|
||||
});
|
||||
Reference in new issue
Block a user