Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IDC-1994] Sort series list by SeriesNumber, and sort by same SeriesNumber by date/time. #2010

Merged
merged 4 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ const OHIFDicomHtmlSopClassHandler = {
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();

const {
SeriesDate,
SeriesTime,
SeriesNumber,
} = instance._instance.metadata;

return {
plugin: 'html',
Modality: 'SR',
Expand All @@ -31,6 +37,9 @@ const OHIFDicomHtmlSopClassHandler = {
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
SeriesDate,
SeriesTime,
SeriesNumber,
authorizationHeaders,
};
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const DicomMicroscopySopClassHandler = {
getDisplaySetFromSeries(series, study, dicomWebClient) {
const instance = series.getFirstInstance();

const {
ContentDate,
ContentTime,
SeriesNumber,
} = instance._instance.metadata;

// Note: We are passing the dicomweb client into each viewport!

return {
Expand All @@ -22,6 +28,9 @@ const DicomMicroscopySopClassHandler = {
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
SeriesDate: ContentDate, // Map ContentDate/Time to SeriesTime for series list sorting.
SeriesTime: ContentTime,
SeriesNumber,
};
},
};
Expand Down
9 changes: 9 additions & 0 deletions extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ const OHIFDicomPDFSopClassHandler = {
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();

const {
ContentDate,
ContentTime,
SeriesNumber,
} = instance._instance.metadata;

return {
plugin: 'pdf',
Modality: 'DOC',
Expand All @@ -21,6 +27,9 @@ const OHIFDicomPDFSopClassHandler = {
SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(),
SeriesDate: ContentDate, // Map ContentDate/Time to SeriesTime for series list sorting.
SeriesTime: ContentTime,
SeriesNumber,
authorizationHeaders: authorizationHeaders,
};
},
Expand Down
2 changes: 2 additions & 0 deletions extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const OHIFDicomRTStructSopClassHandler = {
const {
SeriesDate,
SeriesTime,
SeriesNumber,
SeriesDescription,
FrameOfReferenceUID,
SOPInstanceUID,
Expand All @@ -53,6 +54,7 @@ const OHIFDicomRTStructSopClassHandler = {
isLoaded: false,
SeriesDate,
SeriesTime,
SeriesNumber,
SeriesDescription,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default function getSopClassHandlerModule({ servicesManager }) {
const {
SeriesDate,
SeriesTime,
SeriesNumber,
SeriesDescription,
FrameOfReferenceUID,
SOPInstanceUID,
Expand All @@ -52,6 +53,7 @@ export default function getSopClassHandlerModule({ servicesManager }) {
isLoaded: false,
SeriesDate,
SeriesTime,
SeriesNumber,
SeriesDescription,
};

Expand Down
3 changes: 0 additions & 3 deletions platform/core/src/classes/OHIFStudyMetadataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,6 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
// Get Study display sets
const displaySets = studyMetadata.createDisplaySets();

// Set studyMetadata display sets
studyMetadata.setDisplaySets(displaySets);

OHIFStudyMetadataSource._updateStudyCollections(studyMetadata);
resolve(studyMetadata);
})
Expand Down
211 changes: 94 additions & 117 deletions platform/core/src/classes/metadata/StudyMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from '../../utils/isImage';
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
import errorHandler from '../../errorHandler';
import isLowPriorityModality from '../../utils/isLowPriorityModality';

export class StudyMetadata extends Metadata {
constructor(data, uid) {
Expand Down Expand Up @@ -308,14 +308,10 @@ export class StudyMetadata extends Metadata {
series
);

displaySets.push(...displaySetsForSeries);
displaySetsForSeries.forEach(ds => this._insertDisplaySet(ds));
});

return sortDisplaySetList(displaySets);
}

sortDisplaySets() {
sortDisplaySetList(this._displaySets);
return this._displaySets;
}

/**
Expand Down Expand Up @@ -346,33 +342,17 @@ export class StudyMetadata extends Metadata {
this.addDisplaySet(displaySet);
});

this.sortDisplaySets();

return true;
}

/**
* Set display sets
* @param {Array} displaySets Array of display sets (ImageSet[])
*/
setDisplaySets(displaySets) {
if (Array.isArray(displaySets) && displaySets.length > 0) {
// TODO: This is weird, can we just switch it to writable: true?
this._displaySets.splice(0);

displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
this.sortDisplaySets();
}
}

/**
* Add a single display set to the list
* @param {Object} displaySet Display set object
* @returns {boolean} True on success, false on failure.
*/
addDisplaySet(displaySet) {
if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
this._displaySets.push(displaySet);
this._insertDisplaySet(displaySet);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be nice to pull this bit out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? pull this out where?

return true;
}
return false;
Expand All @@ -393,6 +373,96 @@ export class StudyMetadata extends Metadata {
}
}

/**
* Insert the displaySet so that the list has an increasing SeriesNumber,
* with the most recent series first for displaySets with the same SeriesNumber.
*
* If the displaySet is low priority, the same logic is applied, but is sorted within a sub list
* At the end of the list, where all low priority data is found.
*/
_insertDisplaySet(displaySet) {
const { SeriesNumber } = displaySet;
const displaySets = this._displaySets;
let insertIndex = displaySets.length;
let firstIndexWithSameSeriesNumber;

// If low priority, start search from next low priority.
if (isLowPriorityModality(displaySet.Modality)) {
let startingIndex;

// Find where the first low priority displaySet is.
for (let i = 0; i < displaySets.length; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optimization, so not blocking: we can cache this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can maintain these as separate lists internally, and then provide "displaysets" by joining the two

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points, we can keep further optimisations in mind if speed becomes an issue (maybe with multiple hundred SRs?), but I think we've already spent enough IDC time sorting the list.

if (isLowPriorityModality(displaySets[i].Modality)) {
startingIndex = i;
break;
}
}

if (!startingIndex) {
startingIndex = displaySets.length;
}

// Find the correct SeriesNumber location to insert within the low priority
// Modality displaySets
for (let i = startingIndex; i < displaySets.length; i++) {
if (
displaySets[i].SeriesNumber === SeriesNumber &&
!firstIndexWithSameSeriesNumber
) {
firstIndexWithSameSeriesNumber = i;
}

if (displaySets[i].SeriesNumber > SeriesNumber) {
insertIndex = i;
break;
}
}
} else {
// Find correct SeriesNumber to insert or where the low priority modalities start.
for (let i = 0; i < displaySets.length; i++) {
if (
displaySets[i].SeriesNumber === SeriesNumber &&
!firstIndexWithSameSeriesNumber
) {
firstIndexWithSameSeriesNumber = i;
}

if (
displaySets[i].SeriesNumber > SeriesNumber ||
isLowPriorityModality(displaySets[i].Modality)
) {
insertIndex = i;
break;
}
}
}

// If we have multiple displaySets with the same series number, find the insert position based on
// SeriesDate and SeriesTime.
if (firstIndexWithSameSeriesNumber !== undefined) {
// If no SeriesDate, is just a placeholder displaySet, just insert anywhere, it will be re-added later.
if (displaySet.SeriesDate) {
const seriesDateTime = `${displaySet.SeriesDate}${displaySet.SeriesTime}`;

for (let i = firstIndexWithSameSeriesNumber; i < insertIndex; i++) {
const displaySetI = displaySets[i];

if (
displaySetI.SeriesDate &&
`${displaySetI.SeriesDate}${displaySetI.SeriesTime}` <
seriesDateTime
) {
insertIndex = i;
break;
}
}
}
}

this._displaySets.splice(insertIndex, 0, displaySet);
this.displaySets = this._displaySets;
}

/**
* Search the associated display sets using the supplied callback as criteria. The callback is passed
* two arguments: display set (an ImageSet instance) and index (the integer
Expand Down Expand Up @@ -548,49 +618,6 @@ export class StudyMetadata extends Metadata {
return this._series.indexOf(series);
}

/**
* It sorts the series based on display sets order. Each series must be an instance
* of SeriesMetadata and each display sets must be an instance of ImageSet.
* Useful example of usage:
* Study data provided by backend does not sort series at all and client-side
* needs series sorted by the same criteria used for sorting display sets.
*/
sortSeriesByDisplaySets() {
// Object for mapping display sets' index by SeriesInstanceUID
const displaySetsMapping = {};

// Loop through each display set to create the mapping
this.forEachDisplaySet((displaySet, index) => {
if (!(displaySet instanceof ImageSet)) {
throw new OHIFError(
`StudyMetadata::sortSeriesByDisplaySets display set at index ${index} is not an instance of ImageSet`
);
}

// In case of multiframe studies, just get the first index occurence
if (displaySetsMapping[displaySet.SeriesInstanceUID] === void 0) {
displaySetsMapping[displaySet.SeriesInstanceUID] = index;
}
});

// Clone of actual series
const actualSeries = this.getSeries();

actualSeries.forEach((series, index) => {
if (!(series instanceof SeriesMetadata)) {
throw new OHIFError(
`StudyMetadata::sortSeriesByDisplaySets series at index ${index} is not an instance of SeriesMetadata`
);
}

// Get the new series index
const seriesIndex = displaySetsMapping[series.getSeriesInstanceUID()];

// Update the series object with the new series position
this._series[seriesIndex] = series;
});
}

/**
* Compares the current study instance with another one.
* @param {StudyMetadata} study An instance of the StudyMetadata class.
Expand Down Expand Up @@ -865,53 +892,3 @@ function _getDisplaySetFromSopClassModule(
}
return displaySet;
}

/**
* Sort series primarily by Modality (i.e., series with references to other
* series like SEG, KO or PR are grouped in the end of the list) and then by
* series number:
*
* --------
* | CT #3 |
* | CT #4 |
* | CT #5 |
* --------
* | SEG #1 |
* | SEG #2 |
* --------
*
* @param {*} a - DisplaySet
* @param {*} b - DisplaySet
*/

function seriesSortingCriteria(a, b) {
const isLowPriorityA = isLowPriorityModality(a.Modality);
const isLowPriorityB = isLowPriorityModality(b.Modality);
if (!isLowPriorityA && isLowPriorityB) {
return -1;
}
if (isLowPriorityA && !isLowPriorityB) {
return 1;
}
return sortBySeriesNumber(a, b);
}

/**
* Sort series by series number. Series with low
* @param {*} a - DisplaySet
* @param {*} b - DisplaySet
*/
function sortBySeriesNumber(a, b) {
const seriesNumberAIsGreaterOrUndefined =
a.SeriesNumber > b.SeriesNumber || (!a.SeriesNumber && b.SeriesNumber);

return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
}

/**
* Sorts a list of display set objects
* @param {Array} list A list of display sets to be sorted
*/
function sortDisplaySetList(list) {
return list.sort(seriesSortingCriteria);
}
9 changes: 8 additions & 1 deletion platform/core/src/utils/isLowPriorityModality.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR']);
const LOW_PRIORITY_MODALITIES = Object.freeze([
'SEG',
'DOC',
'RTSTRUCT',
'SR',
'KO',
'PR',
]);

export default function isLowPriorityModality(Modality) {
return LOW_PRIORITY_MODALITIES.includes(Modality);
Expand Down
Loading