fix: Initial sort not consistent (#5224)
* fix: Initial sort not consistent * Add better sort criteria * Remove sorting changes * Start replacing screen shots * Update bun lock for version * Leave default sort for test mode * fix: Add consistent sorting for same series splits * PR comments * docs: add notes for series/display set sort * Updated example docs * Update sortVector to be compareSameStudy * Add unit test * PR comments
This commit is contained in:
1 parent
14bc99124d
commit
77f9f8e1c4
8 files changed
+253
-66
No files matched your search
@@ -464,12 +464,17 @@ export default class DisplaySetService extends PubSubService {
|
||||
/**
|
||||
*
|
||||
* @param sortFn function to sort the display sets
|
||||
* @param direction direction to sort the display sets
|
||||
* @param direction direction to sort the display sets. Ascending means
|
||||
* increasing in value, which will typically put the lowest series numbers
|
||||
* first, with low priority display sets last with newest first.
|
||||
* The meaning of this flag may change to leave the image/non-image display
|
||||
* set sorting alone and only affect sorting within groups, or have additional
|
||||
* values for specific changes to the sort.
|
||||
* @returns void
|
||||
*/
|
||||
public sortDisplaySets(
|
||||
sortFn: (a: DisplaySet, b: DisplaySet) => number,
|
||||
direction: string,
|
||||
direction: 'ascending' | 'descending' = 'ascending',
|
||||
suppressEvent = false
|
||||
): void {
|
||||
this.activeDisplaySets.sort(sortFn);
|
||||
|
||||
@@ -78,6 +78,12 @@ export type DisplaySet = {
|
||||
isLoaded?: boolean;
|
||||
isHydrated?: boolean;
|
||||
isRehydratable?: boolean;
|
||||
|
||||
/**
|
||||
* The name of the comparison function (for sort) to use when comparing display
|
||||
* sets that are coming from same series instanceUID.
|
||||
*/
|
||||
compareSameSeries?: string;
|
||||
};
|
||||
|
||||
export type DisplaySetSeriesMetadataInvalidatedEvent = {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useSystem } from '../contextProviders/SystemProvider';
|
||||
import { seriesSortCriteria } from './sortStudy';
|
||||
|
||||
/**
|
||||
* Tab properties that drive which tab group is used for thumbnail display.
|
||||
*/
|
||||
export type TabProp = {
|
||||
name: string,
|
||||
label: string,
|
||||
studies: any[],
|
||||
}
|
||||
name: string;
|
||||
label: string;
|
||||
studies: any[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Collection of tab properties with studies presorted depending on tab mod.
|
||||
@@ -36,7 +37,7 @@ export function createStudyBrowserTabs(
|
||||
recentTimeframeMS = 31536000000
|
||||
): TabsProps {
|
||||
const { servicesManager } = useSystem();
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const { displaySetService, customizationService } = servicesManager.services;
|
||||
|
||||
const shouldSortBySeriesUID = process.env.TEST_ENV === 'true';
|
||||
const primaryStudies = [];
|
||||
@@ -48,17 +49,16 @@ export function createStudyBrowserTabs(
|
||||
);
|
||||
|
||||
// sort them by seriesInstanceUID
|
||||
let sortedDisplaySets;
|
||||
if (shouldSortBySeriesUID) {
|
||||
sortedDisplaySets = displaySetsForStudy.sort((a, b) => {
|
||||
const displaySetA = displaySetService.getDisplaySetByUID(a.displaySetInstanceUID);
|
||||
const displaySetB = displaySetService.getDisplaySetByUID(b.displaySetInstanceUID);
|
||||
const sortCriteria = shouldSortBySeriesUID
|
||||
? seriesSortCriteria.compareSeriesUID
|
||||
: (customizationService.getCustomization('sortingCriteria') as (a, b) => number);
|
||||
const sortedDisplaySets = displaySetsForStudy.sort((a, b) => {
|
||||
const displaySetA = displaySetService.getDisplaySetByUID(a.displaySetInstanceUID);
|
||||
const displaySetB = displaySetService.getDisplaySetByUID(b.displaySetInstanceUID);
|
||||
return sortCriteria(displaySetA, displaySetB);
|
||||
});
|
||||
|
||||
return displaySetA.SeriesInstanceUID.localeCompare(displaySetB.SeriesInstanceUID);
|
||||
});
|
||||
} else {
|
||||
sortedDisplaySets = displaySetsForStudy;
|
||||
}
|
||||
// return displaySetA.SeriesInstanceUID.localeCompare(displaySetB.SeriesInstanceUID);
|
||||
|
||||
const tabStudy = Object.assign({}, study, {
|
||||
displaySets: sortedDisplaySets,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { compareSeriesUID, addSameSeriesCompare, compare } from './sortStudy';
|
||||
|
||||
addSameSeriesCompare('default', (a,b) => compare(a.default,b.default), 5);
|
||||
const altCompare = 'altCompare'
|
||||
addSameSeriesCompare(altCompare, (a,b) => compare(a.altCompare,b.altCompare), 3);
|
||||
|
||||
const ds1 = {
|
||||
SeriesInstanceUID: '1',
|
||||
default: 'ds1',
|
||||
}
|
||||
|
||||
const ds2 = {
|
||||
...ds1,
|
||||
default: 'ds2',
|
||||
}
|
||||
|
||||
const ds3 = {
|
||||
...ds2,
|
||||
altCompare: 3,
|
||||
compareSameSeries: altCompare,
|
||||
}
|
||||
|
||||
const ds4 = {
|
||||
...ds1,
|
||||
altCompare:4,
|
||||
compareSameSeries: altCompare,
|
||||
}
|
||||
|
||||
const ds5 = {
|
||||
...ds1,
|
||||
SeriesInstanceUID: '3',
|
||||
}
|
||||
|
||||
describe('sortStudy', () => {
|
||||
test('compareSameSeries',() => {
|
||||
const initial = [ds5, ds4,ds3,ds2,ds1];
|
||||
initial.sort(compareSeriesUID);
|
||||
expect(initial[0]).toBe(ds1);
|
||||
expect(initial[1]).toBe(ds2);
|
||||
expect(initial[2]).toBe(ds3);
|
||||
expect(initial[3]).toBe(ds4);
|
||||
expect(initial[4]).toBe(ds5);
|
||||
})
|
||||
})
|
||||
@@ -3,19 +3,77 @@ import isLowPriorityModality from './isLowPriorityModality';
|
||||
import calculateScanAxisNormal from './calculateScanAxisNormal';
|
||||
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
|
||||
|
||||
const compareSeriesDateTime = (a, b) => {
|
||||
const seriesDateA = Date.parse(`${a.seriesDate ?? a.SeriesDate} ${a.seriesTime ?? a.SeriesTime}`);
|
||||
const seriesDateB = Date.parse(`${b.seriesDate ?? b.SeriesDate} ${b.seriesTime ?? b.SeriesTime}`);
|
||||
return seriesDateA - seriesDateB;
|
||||
export const compare = (a, b) => {
|
||||
if (a == b) return 0;
|
||||
if (!a && b) return -1;
|
||||
if (!b && a) return 1;
|
||||
if (a < b) return -1;
|
||||
return 1;
|
||||
};
|
||||
|
||||
const defaultSeriesSort = (a, b) => {
|
||||
type CompareSameSeries = {
|
||||
priority: number;
|
||||
compare: (a, b) => number;
|
||||
};
|
||||
|
||||
const mapCompareSameSeries = new Map<string, CompareSameSeries>();
|
||||
|
||||
/**
|
||||
* Adds a comparison for same series display sets.
|
||||
* Supply null for compareF to delete the function.
|
||||
*/
|
||||
export function addSameSeriesCompare(name: string, compareF: (a, b) => number, priority: number) {
|
||||
if (!compareF) {
|
||||
mapCompareSameSeries.delete(name);
|
||||
} else {
|
||||
mapCompareSameSeries.set(name, { compare: compareF, priority });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the "series" sort is used on display sets, it is possible to get the
|
||||
* same series twice. This method compares two display sets from the same series
|
||||
*
|
||||
* If both display sets have the same compareSameSeries name, then the
|
||||
* function registered for that name will be used.
|
||||
*
|
||||
* If they differ, then the priority between the two functions will be used.
|
||||
*
|
||||
* Otherwise, the instance compare will be used on the default instance.
|
||||
*
|
||||
* This provides a configurable well defined sorting order.
|
||||
*/
|
||||
export const compareSameSeriesDisplaySet = (a, b) => {
|
||||
const { compareSameSeries: compareAName = 'default' } = a;
|
||||
const { compareSameSeries: compareBName = 'default' } = b;
|
||||
const compareA = mapCompareSameSeries.get(compareAName);
|
||||
const compareB = mapCompareSameSeries.get(compareBName);
|
||||
if (compareA && compareB) {
|
||||
const compareValue =
|
||||
compareA === compareB
|
||||
? compareA.compare(a, b)
|
||||
: compare(compareA.priority, compareB.priority);
|
||||
if (!compareValue) {
|
||||
return compareValue;
|
||||
}
|
||||
}
|
||||
return sortByInstanceNumber(a.instance, b.instance);
|
||||
};
|
||||
|
||||
export const compareSeriesUID = (a, b) =>
|
||||
compare(a.SeriesInstanceUID, b.SeriesInstanceUID) || compareSameSeriesDisplaySet(a, b);
|
||||
|
||||
export const compareSeriesDateTime = (a, b) => {
|
||||
// Natural order of string is good enough here
|
||||
const seriesDateA = `${a.seriesDate ?? a.SeriesDate} ${a.seriesTime ?? a.SeriesTime}`;
|
||||
const seriesDateB = `${b.seriesDate ?? b.SeriesDate} ${b.seriesTime ?? b.SeriesTime}`;
|
||||
return compare(seriesDateA, seriesDateB) || compareSeriesUID(a, b);
|
||||
};
|
||||
|
||||
export const defaultSeriesSort = (a, b) => {
|
||||
const seriesNumberA = a.SeriesNumber ?? a.seriesNumber;
|
||||
const seriesNumberB = b.SeriesNumber ?? b.seriesNumber;
|
||||
if (seriesNumberA === seriesNumberB) {
|
||||
return compareSeriesDateTime(a, b);
|
||||
}
|
||||
return seriesNumberA - seriesNumberB;
|
||||
return compare(seriesNumberA, seriesNumberB) || compareSeriesDateTime(a, b);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -24,14 +82,14 @@ const defaultSeriesSort = (a, b) => {
|
||||
* @param {Object} firstSeries
|
||||
* @param {Object} secondSeries
|
||||
*/
|
||||
function seriesInfoSortingCriteria(firstSeries, secondSeries) {
|
||||
export function seriesInfoSortingCriteria(firstSeries, secondSeries) {
|
||||
const aLowPriority = isLowPriorityModality(firstSeries.Modality ?? firstSeries.modality);
|
||||
const bLowPriority = isLowPriorityModality(secondSeries.Modality ?? secondSeries.modality);
|
||||
|
||||
if (aLowPriority) {
|
||||
// Use the reverse sort order for low priority modalities so that the
|
||||
// most recent one comes up first as usually that is the one of interest.
|
||||
return bLowPriority ? defaultSeriesSort(secondSeries, firstSeries) : 1;
|
||||
return bLowPriority ? compareSeriesDateTime(secondSeries, firstSeries) : 1;
|
||||
} else if (bLowPriority) {
|
||||
return -1;
|
||||
}
|
||||
@@ -39,44 +97,50 @@ function seriesInfoSortingCriteria(firstSeries, secondSeries) {
|
||||
return defaultSeriesSort(firstSeries, secondSeries);
|
||||
}
|
||||
|
||||
const seriesSortCriteria = {
|
||||
export const seriesSortCriteria = {
|
||||
default: seriesInfoSortingCriteria,
|
||||
seriesInfoSortingCriteria,
|
||||
compareSameSeries: compareSameSeriesDisplaySet,
|
||||
compareSeriesDateTime,
|
||||
compareSeriesUID,
|
||||
};
|
||||
|
||||
const sortByInstanceNumber = (a, b) => {
|
||||
// Sort by InstanceNumber (0020,0013)
|
||||
/**
|
||||
* Compares two instances first by instance number, and then by
|
||||
* sop and frame numbers.
|
||||
* Handles undefined values for use with display set comparison.
|
||||
*/
|
||||
export const sortByInstanceNumber = (a, b) => {
|
||||
if (!a || !b) {
|
||||
return (!a && !b && 0) || (!a && -1) || 1;
|
||||
}
|
||||
const aInstance = parseInt(a.InstanceNumber) || 0;
|
||||
const bInstance = parseInt(b.InstanceNumber) || 0;
|
||||
if (aInstance !== bInstance) {
|
||||
return (parseInt(a.InstanceNumber) || 0) - (parseInt(b.InstanceNumber) || 0);
|
||||
}
|
||||
// Fallback rule to enable consistent sorting
|
||||
if (a.SOPInstanceUID === b.SOPInstanceUID) {
|
||||
return 0;
|
||||
}
|
||||
return a.SOPInstanceUID < b.SOPInstanceUID ? -1 : 1;
|
||||
return compare(a.SOPInstanceUID, b.SOPInstanceUID) || compare(a.frameNumber, b.frameNumber);
|
||||
};
|
||||
|
||||
const instancesSortCriteria = {
|
||||
export const instancesSortCriteria = {
|
||||
default: sortByInstanceNumber,
|
||||
sortByInstanceNumber,
|
||||
};
|
||||
|
||||
const sortingCriteria = {
|
||||
export const sortingCriteria = {
|
||||
seriesSortCriteria,
|
||||
instancesSortCriteria,
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts given series (given param is modified)
|
||||
* Sorts given series or display sets
|
||||
* The default criteria is based on series number in ascending order.
|
||||
*
|
||||
* @param {Array} series List of series
|
||||
* @param {function} seriesSortingCriteria method for sorting
|
||||
* @returns {Array} sorted series object
|
||||
* @param series - List of series (modified in place)
|
||||
* @param seriesSortingCriteria - method for sorting
|
||||
* @returns sorted series object
|
||||
*/
|
||||
const sortStudySeries = (
|
||||
export const sortStudySeries = (
|
||||
series,
|
||||
seriesSortingCriteria = seriesSortCriteria.default,
|
||||
sortFunction = null
|
||||
@@ -96,7 +160,7 @@ const sortStudySeries = (
|
||||
* @param {function} instancesSortingCriteria method for sorting
|
||||
* @returns {Array} sorted instancesList object
|
||||
*/
|
||||
const sortStudyInstances = (
|
||||
export const sortStudyInstances = (
|
||||
instancesList,
|
||||
instancesSortingCriteria = instancesSortCriteria.default
|
||||
) => {
|
||||
@@ -113,7 +177,7 @@ const sortStudyInstances = (
|
||||
* @param {function} [instancesSortingCriteria = instancesSortCriteria.default] method for sorting instances
|
||||
* @returns {Object} sorted study object
|
||||
*/
|
||||
export default function sortStudy(
|
||||
export function sortStudy(
|
||||
study,
|
||||
deepSort = true,
|
||||
seriesSortingCriteria = seriesSortCriteria.default,
|
||||
@@ -134,7 +198,7 @@ export default function sortStudy(
|
||||
return study;
|
||||
}
|
||||
|
||||
function isValidForPositionSort(images): boolean {
|
||||
export function isValidForPositionSort(images): boolean {
|
||||
if (images.length <= 1) {
|
||||
return false; // No need to sort if there's only one image
|
||||
}
|
||||
@@ -161,7 +225,7 @@ function isValidForPositionSort(images): boolean {
|
||||
*
|
||||
* @returns images - reference to images after sorting
|
||||
*/
|
||||
const sortImagesByPatientPosition = images => {
|
||||
export const sortImagesByPatientPosition = images => {
|
||||
const referenceImagePositionPatient = images[0].ImagePositionPatient;
|
||||
const imageOrientationPatient = images[0].ImageOrientationPatient;
|
||||
|
||||
@@ -188,13 +252,4 @@ const sortImagesByPatientPosition = images => {
|
||||
return images;
|
||||
};
|
||||
|
||||
export {
|
||||
sortStudy,
|
||||
sortStudySeries,
|
||||
sortStudyInstances,
|
||||
sortingCriteria,
|
||||
seriesSortCriteria,
|
||||
instancesSortCriteria,
|
||||
isValidForPositionSort,
|
||||
sortImagesByPatientPosition,
|
||||
};
|
||||
export default sortStudy;
|
||||
Reference in new issue
Block a user