Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | 204x 83756x 79852x 79852x 79852x 57374x 204x 204x 407x 407x 407x 407x 407x 407x 204x 79027x 204x 204x 204x 1210x 1210x 1210x 1230x 1230x 1230x 1x 1229x 19x 1210x 204x 204x 186024x 186024x 186024x 186024x 182507x 3517x 3495x 22x 204x 204x 8x 8x 8x 8x 8x 8x 281x 281x 8x 204x 200x 200x 204x 1987x 792x 3x 789x 789x 789x 789x 789x 204x 789x 789x 789x 789x 48778x 48778x 48778x 48778x 48990x 789x 48778x 789x | import { vec3 } from 'gl-matrix';
import isLowPriorityModality from './isLowPriorityModality';
import calculateScanAxisNormal from './calculateScanAxisNormal';
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
import { getDateTimeSortKey, getSeriesDateTimeSortKey } from './seriesDateTime';
export const compare = (a, b) => {
if (a == b) return 0;
Iif (!a && b) return -1;
Iif (!b && a) return 1;
if (a < b) return -1;
return 1;
};
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);
Iif (compareA && compareB) {
const compareValue =
compareA === compareB
? compareA.compare(a, b)
: compare(compareA.priority, compareB.priority);
Iif (compareValue) {
return compareValue;
}
}
return sortByInstanceNumber(a.instance, b.instance);
};
export const compareSeriesUID = (a, b) =>
compare(a.SeriesInstanceUID, b.SeriesInstanceUID) || compareSameSeriesDisplaySet(a, b);
/**
* The date/time a display set is ordered by is the display set's own
* `SeriesDate`/`SeriesTime`, which is the *display set* date/time and not
* necessarily the date/time of the series the instances belong to - see the
* `SeriesDate` field of the `DisplaySet` type for the whole contract. The SOP
* class handler writes it with {@link getSeriesDateTime} of the instance the
* display set shows, so a report or a segmentation saved into an existing
* series carries the date/time of that save rather than the date/time the
* series was first created.
*
* The key is read from the display set and never from `displaySet.instance`,
* for two reasons.
*
* `compareSeriesDateTime` falls through to `compareSameSeriesDisplaySet` only
* when this key ties. A key read from the instance differs between the display
* sets of one split series, because each of them shows a different instance, so
* it would order them by the instance each one happens to show and the
* comparison registered for that series would never run.
*
* A key read from the instance also makes the comparator inconsistent. A
* pairwise rule that reads one key inside a series and another between series
* answers A1 < B, B < A2 and A2 < A1 for a series A whose two display sets
* straddle a display set B of another series. That is a cycle, and
* `Array.prototype.sort` then returns a different list for each input order.
* Reading one key for both cases removes the cycle by construction.
*
* A series, as opposed to a display set, carries the same two attributes and is
* ordered by them in the same way.
*/
export const dateTimeSortKey = source =>
getDateTimeSortKey(
source.seriesDate ?? source.SeriesDate,
source.seriesTime ?? source.SeriesTime
);
/**
* Compares by {@link dateTimeSortKey}, oldest first. Sides with no date at all
* sort as the oldest, and a date with no time sorts before the timed values of
* that same date.
*/
export const compareSeriesDateTime = (a, b) =>
compare(dateTimeSortKey(a), dateTimeSortKey(b)) || compareSeriesUID(a, b);
export const defaultSeriesSort = (a, b) => {
const seriesNumberA = a.SeriesNumber ?? a.seriesNumber;
const seriesNumberB = b.SeriesNumber ?? b.seriesNumber;
return compare(seriesNumberA, seriesNumberB) || compareSeriesDateTime(a, b);
};
/**
* Series sorting criteria: series considered low priority are moved to the end
* of the list and series number is used to break ties
* @param {Object} firstSeries
* @param {Object} 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 ? compareSeriesDateTime(secondSeries, firstSeries) : 1;
} else if (bLowPriority) {
return -1;
}
return defaultSeriesSort(firstSeries, secondSeries);
}
export const seriesSortCriteria = {
default: seriesInfoSortingCriteria,
seriesInfoSortingCriteria,
compareSameSeries: compareSameSeriesDisplaySet,
compareSeriesDateTime,
compareSeriesUID,
};
/**
* Compares two instances first by instance number, then by when they were
* created, and then by sop and frame numbers.
* Handles undefined values for use with display set comparison.
*/
export const sortByInstanceNumber = (a, b) => {
Iif (!a || !b) {
// Two missing instances are equal. The `||` chain this replaces treated a
// 0 as "no answer" and fell through to -1, so a pair of display sets that
// both lack an instance compared as -1 in both directions.
Iif (!a && !b) {
return 0;
}
return a ? 1 : -1;
}
const aInstance = parseInt(a.InstanceNumber) || 0;
const bInstance = parseInt(b.InstanceNumber) || 0;
if (aInstance !== bInstance) {
return aInstance - bInstance;
}
// Two frames of one instance share every date/time that instance has, so only
// the frame number orders them. Excluding them first is also what keeps a
// large multi frame series cheap to sort: its frames all carry the same
// instance number, so every pair reaches this point. Sources with no SOP
// instance UID at all - display set view models, whose dates are formatted
// for display rather than comparable - are likewise left as they came in.
if (a.SOPInstanceUID === b.SOPInstanceUID) {
return compare(a.frameNumber, b.frameNumber);
}
// The instance numbers do not order these two - they are the same, or neither
// instance has one - so fall back to when each of them was created. The last
// instance of a series is taken to be the most recently created one, so an
// instance number that fails to say which that is has to be replaced by
// something that does.
return (
compare(getSeriesDateTimeSortKey(a), getSeriesDateTimeSortKey(b)) ||
compare(a.SOPInstanceUID, b.SOPInstanceUID)
);
};
export const instancesSortCriteria = {
default: sortByInstanceNumber,
sortByInstanceNumber,
};
export const sortingCriteria = {
seriesSortCriteria,
instancesSortCriteria,
};
export type SortDisplaySetsCopyOptions = {
/**
* Display sets for this study are sorted by series criteria and listed first;
* all other display sets follow in their original relative order.
*/
studyInstanceUIDFirst?: string;
/** Defaults to {@link seriesSortCriteria.default} (not app customization). */
seriesSortingCriteria?: (a, b) => number;
};
/**
* Returns a new array of display sets sorted by default series order
* ({@link seriesSortCriteria.default} / {@link seriesInfoSortingCriteria}), not
* the app customization. Does not mutate the input.
*
* With `studyInstanceUIDFirst`, only that study's display sets are sorted; they
* are placed before the rest, which keeps source order (e.g. load order).
*/
export function sortDisplaySetsCopy(displaySets, options?: SortDisplaySetsCopyOptions | null) {
const seriesSortingCriteria = options?.seriesSortingCriteria ?? seriesSortCriteria.default;
const studyFirst = options?.studyInstanceUIDFirst;
Iif (!studyFirst) {
return [...displaySets].sort(seriesSortingCriteria);
}
const sameStudy = [];
const otherStudy = [];
for (const ds of displaySets) {
if (ds.StudyInstanceUID === studyFirst) {
sameStudy.push(ds);
} else E{
otherStudy.push(ds);
}
}
return [...[...sameStudy].sort(seriesSortingCriteria), ...otherStudy];
}
/**
* Sorts given series or display sets
* The default criteria is based on series number in ascending order.
*
* @param series - List of series (modified in place)
* @param seriesSortingCriteria - method for sorting
* @returns sorted series object
*/
export const sortStudySeries = (
series,
seriesSortingCriteria = seriesSortCriteria.default,
sortFunction = null
) => {
Iif (typeof sortFunction === 'function') {
return sortFunction(series);
} else {
return series.sort(seriesSortingCriteria);
}
};
/**
* Sorts given instancesList (given param is modified)
* The default criteria is based on instance number in ascending order.
*
* @param {Array} instancesList List of series
* @param {function} instancesSortingCriteria method for sorting
* @returns {Array} sorted instancesList object
*/
export const sortStudyInstances = (
instancesList,
instancesSortingCriteria = instancesSortCriteria.default
) => {
return instancesList.sort(instancesSortingCriteria);
};
/**
* Sorts the series and instances (by default) inside a study instance based on sortingCriteria (given param is modified)
* The default criteria is based on series and instance numbers in ascending order.
*
* @param {Object} study The study instance
* @param {boolean} [deepSort = true] to sort instance also
* @param {function} [seriesSortingCriteria = seriesSortCriteria.default] method for sorting series
* @param {function} [instancesSortingCriteria = instancesSortCriteria.default] method for sorting instances
* @returns {Object} sorted study object
*/
export function sortStudy(
study,
deepSort = true,
seriesSortingCriteria = seriesSortCriteria.default,
instancesSortingCriteria = instancesSortCriteria.default
) {
Iif (!study || !study.series) {
throw new Error('Insufficient study data was provided to sortStudy');
}
sortStudySeries(study.series, seriesSortingCriteria);
Iif (deepSort) {
study.series.forEach(series => {
sortStudyInstances(series.instances, instancesSortingCriteria);
});
}
return study;
}
export function isValidForPositionSort(images): boolean {
if (images.length <= 1) {
return false; // No need to sort if there's only one image
}
// Use the first image as a reference
const referenceImagePositionPatient = images[0].ImagePositionPatient;
const imageOrientationPatient = images[0].ImageOrientationPatient;
Iif (!referenceImagePositionPatient || !imageOrientationPatient) {
return false;
}
Iif (!areAllImageOrientationsEqual(images)) {
return false;
}
return true;
}
/**
* Sort by image position, calculated using imageOrientationPatient and ImagePositionPatient
* If imageOrientationPatient or ImagePositionPatient is not available, Images will be sorted by the provided sortingCriteria
* Note: Images are sorted in-place and a reference to the sorted image array is returned.
*
* @returns images - reference to images after sorting
*/
export const sortImagesByPatientPosition = images => {
const referenceImagePositionPatient = images[0].ImagePositionPatient;
const imageOrientationPatient = images[0].ImageOrientationPatient;
// Calculate the scan axis normal using the cross product
const scanAxisNormal = calculateScanAxisNormal(imageOrientationPatient);
// Compute distances from each image to the reference image
const distanceInstancePairs = images.map(image => {
const imagePositionPatient = image.ImagePositionPatient;
const deltaVector = vec3.create();
const distance = vec3.dot(
scanAxisNormal,
vec3.subtract(deltaVector, imagePositionPatient, referenceImagePositionPatient)
);
return { distance, image };
});
// Sort images based on the computed distances
distanceInstancePairs.sort((a, b) => b.distance - a.distance);
// Reorder the images in the original array
for (const [index, item] of distanceInstancePairs.entries()) {
images[index] = item.image;
}
return images;
};
export default sortStudy;
|