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

* Sort based on SeriesNumber and SeriesDate/SeriesTime.

* Harden, and perform final sort in algorithm if last N entries have the same SeriesNumber.

* Switch to insertion rather than sorting as sorting is too slow. Reimplement low priority sorting into new insertion method.

* Fix local file viewing.
This commit is contained in:
James Petts 2020-08-28 16:51:08 +01:00 committed by GitHub
parent ef28fa2ac1
commit dbf0501031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 133 deletions

View File

@ -22,6 +22,12 @@ const OHIFDicomHtmlSopClassHandler = {
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) { getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance(); const instance = series.getFirstInstance();
const {
SeriesDate,
SeriesTime,
SeriesNumber,
} = instance._instance.metadata;
return { return {
plugin: 'html', plugin: 'html',
Modality: 'SR', Modality: 'SR',
@ -31,6 +37,9 @@ const OHIFDicomHtmlSopClassHandler = {
SOPInstanceUID: instance.getSOPInstanceUID(), SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(), SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(), StudyInstanceUID: study.getStudyInstanceUID(),
SeriesDate,
SeriesTime,
SeriesNumber,
authorizationHeaders, authorizationHeaders,
}; };
}, },

View File

@ -12,6 +12,12 @@ const DicomMicroscopySopClassHandler = {
getDisplaySetFromSeries(series, study, dicomWebClient) { getDisplaySetFromSeries(series, study, dicomWebClient) {
const instance = series.getFirstInstance(); const instance = series.getFirstInstance();
const {
ContentDate,
ContentTime,
SeriesNumber,
} = instance._instance.metadata;
// Note: We are passing the dicomweb client into each viewport! // Note: We are passing the dicomweb client into each viewport!
return { return {
@ -22,6 +28,9 @@ const DicomMicroscopySopClassHandler = {
SOPInstanceUID: instance.getSOPInstanceUID(), SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(), SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(), StudyInstanceUID: study.getStudyInstanceUID(),
SeriesDate: ContentDate, // Map ContentDate/Time to SeriesTime for series list sorting.
SeriesTime: ContentTime,
SeriesNumber,
}; };
}, },
}; };

View File

@ -12,6 +12,12 @@ const OHIFDicomPDFSopClassHandler = {
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) { getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance(); const instance = series.getFirstInstance();
const {
ContentDate,
ContentTime,
SeriesNumber,
} = instance._instance.metadata;
return { return {
plugin: 'pdf', plugin: 'pdf',
Modality: 'DOC', Modality: 'DOC',
@ -21,6 +27,9 @@ const OHIFDicomPDFSopClassHandler = {
SOPInstanceUID: instance.getSOPInstanceUID(), SOPInstanceUID: instance.getSOPInstanceUID(),
SeriesInstanceUID: series.getSeriesInstanceUID(), SeriesInstanceUID: series.getSeriesInstanceUID(),
StudyInstanceUID: study.getStudyInstanceUID(), StudyInstanceUID: study.getStudyInstanceUID(),
SeriesDate: ContentDate, // Map ContentDate/Time to SeriesTime for series list sorting.
SeriesTime: ContentTime,
SeriesNumber,
authorizationHeaders: authorizationHeaders, authorizationHeaders: authorizationHeaders,
}; };
}, },

View File

@ -27,6 +27,7 @@ const OHIFDicomRTStructSopClassHandler = {
const { const {
SeriesDate, SeriesDate,
SeriesTime, SeriesTime,
SeriesNumber,
SeriesDescription, SeriesDescription,
FrameOfReferenceUID, FrameOfReferenceUID,
SOPInstanceUID, SOPInstanceUID,
@ -53,6 +54,7 @@ const OHIFDicomRTStructSopClassHandler = {
isLoaded: false, isLoaded: false,
SeriesDate, SeriesDate,
SeriesTime, SeriesTime,
SeriesNumber,
SeriesDescription, SeriesDescription,
}; };

View File

@ -28,6 +28,7 @@ export default function getSopClassHandlerModule({ servicesManager }) {
const { const {
SeriesDate, SeriesDate,
SeriesTime, SeriesTime,
SeriesNumber,
SeriesDescription, SeriesDescription,
FrameOfReferenceUID, FrameOfReferenceUID,
SOPInstanceUID, SOPInstanceUID,
@ -52,6 +53,7 @@ export default function getSopClassHandlerModule({ servicesManager }) {
isLoaded: false, isLoaded: false,
SeriesDate, SeriesDate,
SeriesTime, SeriesTime,
SeriesNumber,
SeriesDescription, SeriesDescription,
}; };

View File

@ -54,9 +54,6 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
// Get Study display sets // Get Study display sets
const displaySets = studyMetadata.createDisplaySets(); const displaySets = studyMetadata.createDisplaySets();
// Set studyMetadata display sets
studyMetadata.setDisplaySets(displaySets);
OHIFStudyMetadataSource._updateStudyCollections(studyMetadata); OHIFStudyMetadataSource._updateStudyCollections(studyMetadata);
resolve(studyMetadata); resolve(studyMetadata);
}) })

View File

@ -10,8 +10,8 @@ import { api } from 'dicomweb-client';
// - createStacks // - createStacks
import { isImage } from '../../utils/isImage'; import { isImage } from '../../utils/isImage';
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable'; import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
import errorHandler from '../../errorHandler'; import errorHandler from '../../errorHandler';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata { export class StudyMetadata extends Metadata {
constructor(data, uid) { constructor(data, uid) {
@ -308,14 +308,10 @@ export class StudyMetadata extends Metadata {
series series
); );
displaySets.push(...displaySetsForSeries); displaySetsForSeries.forEach(ds => this._insertDisplaySet(ds));
}); });
return sortDisplaySetList(displaySets); return this._displaySets;
}
sortDisplaySets() {
sortDisplaySetList(this._displaySets);
} }
/** /**
@ -346,25 +342,9 @@ export class StudyMetadata extends Metadata {
this.addDisplaySet(displaySet); this.addDisplaySet(displaySet);
}); });
this.sortDisplaySets();
return true; 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 * Add a single display set to the list
* @param {Object} displaySet Display set object * @param {Object} displaySet Display set object
@ -372,7 +352,7 @@ export class StudyMetadata extends Metadata {
*/ */
addDisplaySet(displaySet) { addDisplaySet(displaySet) {
if (displaySet instanceof ImageSet || displaySet.sopClassModule) { if (displaySet instanceof ImageSet || displaySet.sopClassModule) {
this._displaySets.push(displaySet); this._insertDisplaySet(displaySet);
return true; return true;
} }
return false; return false;
@ -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++) {
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 * 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 * two arguments: display set (an ImageSet instance) and index (the integer
@ -548,49 +618,6 @@ export class StudyMetadata extends Metadata {
return this._series.indexOf(series); 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. * Compares the current study instance with another one.
* @param {StudyMetadata} study An instance of the StudyMetadata class. * @param {StudyMetadata} study An instance of the StudyMetadata class.
@ -865,53 +892,3 @@ function _getDisplaySetFromSopClassModule(
} }
return displaySet; 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);
}

View File

@ -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) { export default function isLowPriorityModality(Modality) {
return LOW_PRIORITY_MODALITIES.includes(Modality); return LOW_PRIORITY_MODALITIES.includes(Modality);

View File

@ -19,6 +19,7 @@ import UserManagerContext from '../context/UserManagerContext';
import AppContext from '../context/AppContext'; import AppContext from '../context/AppContext';
import './Viewer.css'; import './Viewer.css';
import { finished } from 'stream';
class Viewer extends Component { class Viewer extends Component {
static propTypes = { static propTypes = {
@ -189,6 +190,7 @@ class Viewer extends Component {
currentTimepointId, currentTimepointId,
]); ]);
} }
this.setState({ this.setState({
thumbnails: _mapStudiesToThumbnails(studies), thumbnails: _mapStudiesToThumbnails(studies),
}); });
@ -197,6 +199,7 @@ class Viewer extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { studies, isStudyLoaded } = this.props; const { studies, isStudyLoaded } = this.props;
if (studies !== prevProps.studies) { if (studies !== prevProps.studies) {
this.setState({ this.setState({
thumbnails: _mapStudiesToThumbnails(studies), thumbnails: _mapStudiesToThumbnails(studies),
@ -355,7 +358,6 @@ export default withDialog(Viewer);
* a mapping layer? * a mapping layer?
* *
* TODO[react]: * TODO[react]:
* - Add sorting of display sets
* - Add showStackLoadingProgressBar option * - Add showStackLoadingProgressBar option
* *
* @param {Study[]} studies * @param {Study[]} studies
@ -369,9 +371,9 @@ const _mapStudiesToThumbnails = function(studies) {
const { const {
displaySetInstanceUID, displaySetInstanceUID,
SeriesDescription, SeriesDescription,
SeriesNumber,
InstanceNumber, InstanceNumber,
numImageFrames, numImageFrames,
SeriesNumber,
} = displaySet; } = displaySet;
let imageId; let imageId;
@ -395,9 +397,9 @@ const _mapStudiesToThumbnails = function(studies) {
altImageText, altImageText,
displaySetInstanceUID, displaySetInstanceUID,
SeriesDescription, SeriesDescription,
SeriesNumber,
InstanceNumber, InstanceNumber,
numImageFrames, numImageFrames,
SeriesNumber,
}; };
}); });

View File

@ -77,7 +77,8 @@ class ViewerLocalFileData extends Component {
study.displaySets = study.displaySets =
study.displaySets || study.displaySets ||
studyMetadata.createDisplaySets(sopClassHandlerModules); studyMetadata.createDisplaySets(sopClassHandlerModules);
studyMetadata.setDisplaySets(study.displaySets);
debugger;
studyMetadata.forEachDisplaySet(displayset => { studyMetadata.forEachDisplaySet(displayset => {
displayset.localFile = true; displayset.localFile = true;

View File

@ -47,7 +47,6 @@ const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
let promoted = false; let promoted = false;
// Promote only if no filter should be applied // Promote only if no filter should be applied
if (!isFilterStrategy) { if (!isFilterStrategy) {
_sortStudyDisplaySet(study, studyMetadata);
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters); promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
} }
@ -193,12 +192,6 @@ const _updateStudyDisplaySets = (study, studyMetadata) => {
if (study.derivedDisplaySets) { if (study.derivedDisplaySets) {
studyMetadata._addDerivedDisplaySets(study.derivedDisplaySets); studyMetadata._addDerivedDisplaySets(study.derivedDisplaySets);
} }
studyMetadata.setDisplaySets(study.displaySets);
};
const _sortStudyDisplaySet = (study, studyMetadata) => {
studyMetadata.sortDisplaySets(study.displaySets);
}; };
const _thinStudyData = study => { const _thinStudyData = study => {

View File

@ -194,7 +194,6 @@ const _mapStudiesToNewFormat = studies => {
study.displaySets = study.displaySets =
study.displaySets || study.displaySets ||
studyMetadata.createDisplaySets(sopClassHandlerModules); studyMetadata.createDisplaySets(sopClassHandlerModules);
studyMetadata.setDisplaySets(study.displaySets);
studyMetadataManager.add(studyMetadata); studyMetadataManager.add(studyMetadata);
uniqueStudyUIDs.add(study.StudyInstanceUID); uniqueStudyUIDs.add(study.StudyInstanceUID);