Skip to content

Commit 9e10c2b

Browse files
emfolswederik
authored andcommitted
feat: Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once (#953)
1 parent c5cbd77 commit 9e10c2b

12 files changed

Lines changed: 665 additions & 404 deletions

File tree

platform/core/src/__mocks__/dicomweb-client.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// import { api } from 'dicomweb-client'
22

33
const api = {
4-
DICOMwebClient: jest.fn().mockImplementation(() => {
5-
return {
6-
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
7-
};
4+
DICOMwebClient: jest.fn().mockImplementation(function() {
5+
this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
6+
this.retrieveSeriesMetadata = jest.fn(function(options) {
7+
const { studyInstanceUID, seriesInstanceUID } = options;
8+
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
9+
});
810
}),
911
};
1012

platform/core/src/classes/metadata/StudyMetadata.js

Lines changed: 167 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { SeriesMetadata } from './SeriesMetadata';
88
// - createStacks
99
import { api } from 'dicomweb-client';
1010
// - createStacks
11-
import { isImage } from './../../utils/isImage';
11+
import { isImage } from '../../utils/isImage';
12+
import isLowPriorityModality from '../../utils/isLowPriorityModality';
1213

1314
export class StudyMetadata extends Metadata {
1415
constructor(data, uid) {
@@ -88,6 +89,100 @@ export class StudyMetadata extends Metadata {
8889
return this._displaySets.slice();
8990
}
9091

92+
/**
93+
* Split a series metadata object into display sets
94+
* @param {Array} sopClassHandlerModules List of SOP Class Modules
95+
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
96+
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
97+
* @returns {Array} The list of display sets created for the given series object
98+
*/
99+
_createDisplaySetsForSeries(
100+
sopClassHandlerModules,
101+
series,
102+
givenDisplaySets
103+
) {
104+
const study = this;
105+
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
106+
const anyInstances = series.getInstanceCount() > 0;
107+
108+
if (!anyInstances) {
109+
return;
110+
}
111+
112+
const sopClassUids = getSopClassUids(series);
113+
114+
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
115+
const displaySet = _getDisplaySetFromSopClassModule(
116+
sopClassHandlerModules,
117+
series,
118+
study,
119+
sopClassUids
120+
);
121+
if (displaySet) {
122+
displaySet.sopClassModule = true;
123+
displaySets.push(displaySet);
124+
return;
125+
}
126+
}
127+
128+
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
129+
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
130+
131+
// Search through the instances (InstanceMetadata object) of this series
132+
// Split Multi-frame instances and Single-image modalities
133+
// into their own specific display sets. Place the rest of each
134+
// series into another display set.
135+
const stackableInstances = [];
136+
series.forEachInstance(instance => {
137+
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
138+
if (
139+
!isImage(instance.getRawValue('x00080016')) &&
140+
!instance.getRawValue('x00280010')
141+
) {
142+
return;
143+
}
144+
145+
let displaySet;
146+
147+
if (isMultiFrame(instance)) {
148+
displaySet = makeDisplaySet(series, [instance]);
149+
displaySet.setAttributes({
150+
sopClassUids,
151+
isClip: true,
152+
seriesInstanceUid: series.getSeriesInstanceUID(),
153+
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
154+
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
155+
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
156+
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
157+
});
158+
displaySets.push(displaySet);
159+
} else if (isSingleImageModality(instance.modality)) {
160+
displaySet = makeDisplaySet(series, [instance]);
161+
displaySet.setAttributes({
162+
sopClassUids,
163+
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
164+
seriesInstanceUid: series.getSeriesInstanceUID(),
165+
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
166+
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
167+
});
168+
displaySets.push(displaySet);
169+
} else {
170+
stackableInstances.push(instance);
171+
}
172+
});
173+
174+
if (stackableInstances.length) {
175+
const displaySet = makeDisplaySet(series, stackableInstances);
176+
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
177+
displaySet.setAttributes({
178+
sopClassUids,
179+
});
180+
displaySets.push(displaySet);
181+
}
182+
183+
return displaySets;
184+
}
185+
91186
/**
92187
* Creates a set of series to be placed in the Study Metadata
93188
* The series that appear in the Study Metadata must represent
@@ -101,113 +196,55 @@ export class StudyMetadata extends Metadata {
101196
* @returns {Array} An array of series to be placed in the Study Metadata
102197
*/
103198
createDisplaySets(sopClassHandlerModules) {
104-
const study = this;
105199
const displaySets = [];
106-
const anyDisplaySets = study.getSeriesCount();
107-
const anySopClassHandlerModules =
108-
sopClassHandlerModules && sopClassHandlerModules.length > 0;
200+
const anyDisplaySets = this.getSeriesCount();
109201

110202
if (!anyDisplaySets) {
111203
return displaySets;
112204
}
113205

114206
// Loop through the series (SeriesMetadata)
115-
this.forEachSeries(series => {
116-
const anyInstances = series.getInstanceCount() > 0;
117-
if (!anyInstances) {
118-
return;
119-
}
120-
121-
const sopClassUids = getSopClassUids(series);
122-
123-
if (anySopClassHandlerModules) {
124-
const displaySet = _getDisplaySetFromSopClassModule(
207+
this.forEachSeries(
208+
series =>
209+
void this._createDisplaySetsForSeries(
125210
sopClassHandlerModules,
126211
series,
127-
study,
128-
sopClassUids
129-
);
130-
131-
if (displaySet) {
132-
displaySet.sopClassModule = true;
133-
displaySets.push(displaySet);
134-
135-
return;
136-
}
137-
}
138-
139-
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
140-
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
141-
142-
// Search through the instances (InstanceMetadata object) of this series
143-
// Split Multi-frame instances and Single-image modalities
144-
// into their own specific display sets. Place the rest of each
145-
// series into another display set.
146-
const stackableInstances = [];
147-
series.forEachInstance(instance => {
148-
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
149-
if (
150-
!isImage(instance.getRawValue('x00080016')) &&
151-
!instance.getRawValue('x00280010')
152-
) {
153-
return;
154-
}
155-
156-
let displaySet;
157-
158-
if (isMultiFrame(instance)) {
159-
displaySet = makeDisplaySet(series, [instance]);
160-
displaySet.setAttributes({
161-
sopClassUids,
162-
isClip: true,
163-
seriesInstanceUid: series.getSeriesInstanceUID(),
164-
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
165-
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
166-
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
167-
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
168-
});
169-
displaySets.push(displaySet);
170-
} else if (isSingleImageModality(instance.modality)) {
171-
displaySet = makeDisplaySet(series, [instance]);
172-
displaySet.setAttributes({
173-
sopClassUids,
174-
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
175-
seriesInstanceUid: series.getSeriesInstanceUID(),
176-
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
177-
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
178-
});
179-
displaySets.push(displaySet);
180-
} else {
181-
stackableInstances.push(instance);
182-
}
183-
});
212+
displaySets
213+
)
214+
);
184215

185-
if (stackableInstances.length) {
186-
const displaySet = makeDisplaySet(series, stackableInstances);
187-
displaySet.setAttribute(
188-
'studyInstanceUid',
189-
study.getStudyInstanceUID()
190-
);
191-
displaySet.setAttributes({
192-
sopClassUids,
193-
});
194-
displaySets.push(displaySet);
195-
}
196-
});
216+
return sortDisplaySetList(displaySets);
217+
}
197218

198-
// TODO
199-
displaySets.sort(_sortBySeriesNumber);
200-
displaySets.sort(_sortSegToEndOfList);
219+
sortDisplaySets() {
220+
sortDisplaySetList(this._displaySets);
221+
}
201222

202-
return displaySets;
223+
/**
224+
* Method to append display sets from a given series to the internal list of display sets
225+
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
226+
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
227+
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
228+
*/
229+
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
230+
if (this.containsSeries(series)) {
231+
this.setDisplaySets(
232+
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
233+
);
234+
return true;
235+
}
236+
return false;
203237
}
204238

205239
/**
206240
* Set display sets
207241
* @param {Array} displaySets Array of display sets (ImageSet[])
208242
*/
209243
setDisplaySets(displaySets) {
210-
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
244+
if (Array.isArray(displaySets) && displaySets.length > 0) {
245+
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
246+
this.sortDisplaySets();
247+
}
211248
}
212249

213250
/**
@@ -321,6 +358,12 @@ export class StudyMetadata extends Metadata {
321358
return found;
322359
}
323360

361+
containsSeries(series) {
362+
return (
363+
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
364+
);
365+
}
366+
324367
/**
325368
* Retrieve the number of series within the current study.
326369
* @returns {number} The number of series in the current study.
@@ -651,34 +694,51 @@ function _getDisplaySetFromSopClassModule(
651694
}
652695

653696
/**
697+
* Sort series primarily by modality (i.e., series with references to other
698+
* series like SEG, KO or PR are grouped in the end of the list) and then by
699+
* series number:
700+
*
701+
* --------
702+
* | CT #3 |
703+
* | CT #4 |
704+
* | CT #5 |
705+
* --------
706+
* | SEG #1 |
707+
* | SEG #2 |
708+
* --------
654709
*
655710
* @param {*} a - DisplaySet
656711
* @param {*} b - DisplaySet
657712
*/
658-
function _sortBySeriesNumber(a, b) {
659-
const seriesNumberAIsGreaterOrUndefined =
660-
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
661713

662-
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
714+
function seriesSortingCriteria(a, b) {
715+
const isLowPriorityA = isLowPriorityModality(a.modality);
716+
const isLowPriorityB = isLowPriorityModality(b.modality);
717+
if (!isLowPriorityA && isLowPriorityB) {
718+
return -1;
719+
}
720+
if (isLowPriorityA && !isLowPriorityB) {
721+
return 1;
722+
}
723+
return sortBySeriesNumber(a, b);
663724
}
664725

665726
/**
666-
* Move Segmentation modality files to the end of the list of
667-
* display sets. This is a workaround to prevent issues when
668-
* the referenced dataset's metadata is not yet available.
669-
*
670-
* It will be removed once proper SEG ingestion is added.
671-
*
727+
* Sort series by series number. Series with low
672728
* @param {*} a - DisplaySet
673729
* @param {*} b - DisplaySet
674730
*/
675-
function _sortSegToEndOfList(a, b) {
676-
const displaySetAIsSeg = a.modality === 'SEG';
677-
const displaySetBIsSeg = b.modality === 'SEG';
731+
function sortBySeriesNumber(a, b) {
732+
const seriesNumberAIsGreaterOrUndefined =
733+
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
678734

679-
if (displaySetAIsSeg && displaySetBIsSeg) {
680-
return 0;
681-
}
735+
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
736+
}
682737

683-
return displaySetAIsSeg ? 1 : -1;
684-
}
738+
/**
739+
* Sorts a list of display set objects
740+
* @param {Array} list A list of display sets to be sorted
741+
*/
742+
function sortDisplaySetList(list) {
743+
return list.sort(seriesSortingCriteria);
744+
}

0 commit comments

Comments
 (0)