feat: Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once (#953)
This commit is contained in:
parent
c5cbd77cb9
commit
9e10c2b2de
@ -1,10 +1,12 @@
|
|||||||
// import { api } from 'dicomweb-client'
|
// import { api } from 'dicomweb-client'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
DICOMwebClient: jest.fn().mockImplementation(() => {
|
DICOMwebClient: jest.fn().mockImplementation(function() {
|
||||||
return {
|
this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
|
||||||
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
|
this.retrieveSeriesMetadata = jest.fn(function(options) {
|
||||||
};
|
const { studyInstanceUID, seriesInstanceUID } = options;
|
||||||
|
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import { SeriesMetadata } from './SeriesMetadata';
|
|||||||
// - createStacks
|
// - createStacks
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
// - createStacks
|
// - createStacks
|
||||||
import { isImage } from './../../utils/isImage';
|
import { isImage } from '../../utils/isImage';
|
||||||
|
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||||
|
|
||||||
export class StudyMetadata extends Metadata {
|
export class StudyMetadata extends Metadata {
|
||||||
constructor(data, uid) {
|
constructor(data, uid) {
|
||||||
@ -88,6 +89,100 @@ export class StudyMetadata extends Metadata {
|
|||||||
return this._displaySets.slice();
|
return this._displaySets.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a series metadata object into display sets
|
||||||
|
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
||||||
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
|
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
|
||||||
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
|
*/
|
||||||
|
_createDisplaySetsForSeries(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series,
|
||||||
|
givenDisplaySets
|
||||||
|
) {
|
||||||
|
const study = this;
|
||||||
|
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
|
||||||
|
const anyInstances = series.getInstanceCount() > 0;
|
||||||
|
|
||||||
|
if (!anyInstances) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopClassUids = getSopClassUids(series);
|
||||||
|
|
||||||
|
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
|
||||||
|
const displaySet = _getDisplaySetFromSopClassModule(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
sopClassUids
|
||||||
|
);
|
||||||
|
if (displaySet) {
|
||||||
|
displaySet.sopClassModule = true;
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
||||||
|
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
||||||
|
|
||||||
|
// Search through the instances (InstanceMetadata object) of this series
|
||||||
|
// Split Multi-frame instances and Single-image modalities
|
||||||
|
// into their own specific display sets. Place the rest of each
|
||||||
|
// series into another display set.
|
||||||
|
const stackableInstances = [];
|
||||||
|
series.forEachInstance(instance => {
|
||||||
|
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
|
||||||
|
if (
|
||||||
|
!isImage(instance.getRawValue('x00080016')) &&
|
||||||
|
!instance.getRawValue('x00280010')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (isMultiFrame(instance)) {
|
||||||
|
displaySet = makeDisplaySet(series, [instance]);
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
isClip: true,
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
|
||||||
|
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
|
||||||
|
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
||||||
|
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
} else if (isSingleImageModality(instance.modality)) {
|
||||||
|
displaySet = makeDisplaySet(series, [instance]);
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
||||||
|
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
} else {
|
||||||
|
stackableInstances.push(instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stackableInstances.length) {
|
||||||
|
const displaySet = makeDisplaySet(series, stackableInstances);
|
||||||
|
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return displaySets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a set of series to be placed in the Study Metadata
|
* Creates a set of series to be placed in the Study Metadata
|
||||||
* The series that appear in the Study Metadata must represent
|
* The series that appear in the Study Metadata must represent
|
||||||
@ -101,105 +196,44 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @returns {Array} An array of series to be placed in the Study Metadata
|
* @returns {Array} An array of series to be placed in the Study Metadata
|
||||||
*/
|
*/
|
||||||
createDisplaySets(sopClassHandlerModules) {
|
createDisplaySets(sopClassHandlerModules) {
|
||||||
const study = this;
|
|
||||||
const displaySets = [];
|
const displaySets = [];
|
||||||
const anyDisplaySets = study.getSeriesCount();
|
const anyDisplaySets = this.getSeriesCount();
|
||||||
const anySopClassHandlerModules =
|
|
||||||
sopClassHandlerModules && sopClassHandlerModules.length > 0;
|
|
||||||
|
|
||||||
if (!anyDisplaySets) {
|
if (!anyDisplaySets) {
|
||||||
return displaySets;
|
return displaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the series (SeriesMetadata)
|
// Loop through the series (SeriesMetadata)
|
||||||
this.forEachSeries(series => {
|
this.forEachSeries(
|
||||||
const anyInstances = series.getInstanceCount() > 0;
|
series =>
|
||||||
if (!anyInstances) {
|
void this._createDisplaySetsForSeries(
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopClassUids = getSopClassUids(series);
|
|
||||||
|
|
||||||
if (anySopClassHandlerModules) {
|
|
||||||
const displaySet = _getDisplaySetFromSopClassModule(
|
|
||||||
sopClassHandlerModules,
|
sopClassHandlerModules,
|
||||||
series,
|
series,
|
||||||
study,
|
displaySets
|
||||||
sopClassUids
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displaySet) {
|
return sortDisplaySetList(displaySets);
|
||||||
displaySet.sopClassModule = true;
|
}
|
||||||
displaySets.push(displaySet);
|
|
||||||
|
|
||||||
return;
|
sortDisplaySets() {
|
||||||
}
|
sortDisplaySetList(this._displaySets);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
/**
|
||||||
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
* Method to append display sets from a given series to the internal list of display sets
|
||||||
|
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
|
||||||
// Search through the instances (InstanceMetadata object) of this series
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
// Split Multi-frame instances and Single-image modalities
|
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
||||||
// into their own specific display sets. Place the rest of each
|
*/
|
||||||
// series into another display set.
|
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||||
const stackableInstances = [];
|
if (this.containsSeries(series)) {
|
||||||
series.forEachInstance(instance => {
|
this.setDisplaySets(
|
||||||
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
|
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||||
if (
|
);
|
||||||
!isImage(instance.getRawValue('x00080016')) &&
|
return true;
|
||||||
!instance.getRawValue('x00280010')
|
}
|
||||||
) {
|
return false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let displaySet;
|
|
||||||
|
|
||||||
if (isMultiFrame(instance)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
isClip: true,
|
|
||||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
|
||||||
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
|
|
||||||
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
|
|
||||||
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
|
||||||
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else if (isSingleImageModality(instance.modality)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
|
|
||||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
|
||||||
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
|
||||||
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else {
|
|
||||||
stackableInstances.push(instance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (stackableInstances.length) {
|
|
||||||
const displaySet = makeDisplaySet(series, stackableInstances);
|
|
||||||
displaySet.setAttribute(
|
|
||||||
'studyInstanceUid',
|
|
||||||
study.getStudyInstanceUID()
|
|
||||||
);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
displaySets.sort(_sortBySeriesNumber);
|
|
||||||
displaySets.sort(_sortSegToEndOfList);
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -207,7 +241,10 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @param {Array} displaySets Array of display sets (ImageSet[])
|
* @param {Array} displaySets Array of display sets (ImageSet[])
|
||||||
*/
|
*/
|
||||||
setDisplaySets(displaySets) {
|
setDisplaySets(displaySets) {
|
||||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
||||||
|
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||||
|
this.sortDisplaySets();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -321,6 +358,12 @@ export class StudyMetadata extends Metadata {
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containsSeries(series) {
|
||||||
|
return (
|
||||||
|
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the number of series within the current study.
|
* Retrieve the number of series within the current study.
|
||||||
* @returns {number} The number of series in the current study.
|
* @returns {number} The number of series in the current study.
|
||||||
@ -651,11 +694,41 @@ function _getDisplaySetFromSopClassModule(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* 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 {*} a - DisplaySet
|
||||||
* @param {*} b - DisplaySet
|
* @param {*} b - DisplaySet
|
||||||
*/
|
*/
|
||||||
function _sortBySeriesNumber(a, b) {
|
|
||||||
|
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 =
|
const seriesNumberAIsGreaterOrUndefined =
|
||||||
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
||||||
|
|
||||||
@ -663,22 +736,9 @@ function _sortBySeriesNumber(a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move Segmentation modality files to the end of the list of
|
* Sorts a list of display set objects
|
||||||
* display sets. This is a workaround to prevent issues when
|
* @param {Array} list A list of display sets to be sorted
|
||||||
* the referenced dataset's metadata is not yet available.
|
|
||||||
*
|
|
||||||
* It will be removed once proper SEG ingestion is added.
|
|
||||||
*
|
|
||||||
* @param {*} a - DisplaySet
|
|
||||||
* @param {*} b - DisplaySet
|
|
||||||
*/
|
*/
|
||||||
function _sortSegToEndOfList(a, b) {
|
function sortDisplaySetList(list) {
|
||||||
const displaySetAIsSeg = a.modality === 'SEG';
|
return list.sort(seriesSortingCriteria);
|
||||||
const displaySetBIsSeg = b.modality === 'SEG';
|
}
|
||||||
|
|
||||||
if (displaySetAIsSeg && displaySetBIsSeg) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return displaySetAIsSeg ? 1 : -1;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../../../DICOMWeb/';
|
import DICOMWeb from '../../../DICOMWeb/';
|
||||||
|
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
|
||||||
|
|
||||||
|
const INFO = Symbol('INFO');
|
||||||
|
|
||||||
const WADOProxy = {
|
const WADOProxy = {
|
||||||
convertURL: (url, server) => {
|
convertURL: (url, server) => {
|
||||||
@ -94,9 +97,7 @@ function buildInstanceWadoRsUri(
|
|||||||
seriesInstanceUid,
|
seriesInstanceUid,
|
||||||
sopInstanceUid
|
sopInstanceUid
|
||||||
) {
|
) {
|
||||||
return `${
|
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||||
server.wadoRoot
|
|
||||||
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInstanceFrameWadoRsUri(
|
function buildInstanceFrameWadoRsUri(
|
||||||
@ -166,9 +167,9 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const arrayBufferToPaletteColorLUT = result => {
|
const arrayBufferToPaletteColorLUT = result => {
|
||||||
const arraybuffer = result[0]
|
const arraybuffer = result[0];
|
||||||
const byteArray = new Uint8Array(arraybuffer)
|
const byteArray = new Uint8Array(arraybuffer);
|
||||||
const lut = []
|
const lut = [];
|
||||||
|
|
||||||
for (let i = 0; i < numLutEntries; i++) {
|
for (let i = 0; i < numLutEntries; i++) {
|
||||||
if (bits === 16) {
|
if (bits === 16) {
|
||||||
@ -271,200 +272,338 @@ function getRadiopharmaceuticalInfo(instance) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function makeSOPInstance(server, study, instance) {
|
||||||
|
const { studyInstanceUid } = study;
|
||||||
|
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||||
|
let series = study.seriesMap[seriesInstanceUid];
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
series = {
|
||||||
|
seriesInstanceUid,
|
||||||
|
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||||
|
modality: DICOMWeb.getString(instance['00080060']),
|
||||||
|
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||||
|
seriesDate: DICOMWeb.getString(instance['00080021']),
|
||||||
|
seriesTime: DICOMWeb.getString(instance['00080031']),
|
||||||
|
instances: [],
|
||||||
|
};
|
||||||
|
study.seriesMap[seriesInstanceUid] = series;
|
||||||
|
study.seriesList.push(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||||
|
const wadouri = buildInstanceWadoUrl(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
const baseWadoRsUri = buildInstanceWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
const wadorsuri = buildInstanceFrameWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
const sopInstance = {
|
||||||
|
imageType: DICOMWeb.getString(instance['00080008']),
|
||||||
|
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||||
|
modality: DICOMWeb.getString(instance['00080060']),
|
||||||
|
sopInstanceUid,
|
||||||
|
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||||
|
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||||
|
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||||
|
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||||
|
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||||
|
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||||
|
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||||
|
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
||||||
|
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||||
|
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||||
|
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||||
|
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
||||||
|
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||||
|
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||||
|
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||||
|
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||||
|
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
||||||
|
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
||||||
|
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||||
|
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||||
|
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||||
|
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||||
|
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
||||||
|
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||||
|
laterality: DICOMWeb.getString(instance['00200062']),
|
||||||
|
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||||
|
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
||||||
|
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||||
|
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
||||||
|
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||||
|
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
|
||||||
|
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
||||||
|
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
||||||
|
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
||||||
|
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
||||||
|
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
||||||
|
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
||||||
|
echoNumber: DICOMWeb.getString(instance['00180086']),
|
||||||
|
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
||||||
|
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
||||||
|
baseWadoRsUri: baseWadoRsUri,
|
||||||
|
wadouri: WADOProxy.convertURL(wadouri, server),
|
||||||
|
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
||||||
|
wadoRoot: server.wadoRoot,
|
||||||
|
imageRendering: server.imageRendering,
|
||||||
|
thumbnailRendering: server.thumbnailRendering,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
||||||
|
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
|
||||||
|
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281101'])
|
||||||
|
);
|
||||||
|
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281102'])
|
||||||
|
);
|
||||||
|
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281103'])
|
||||||
|
);
|
||||||
|
const palettes = await getPaletteColors(
|
||||||
|
server,
|
||||||
|
instance,
|
||||||
|
redPaletteColorLookupTableDescriptor
|
||||||
|
);
|
||||||
|
|
||||||
|
if (palettes) {
|
||||||
|
if (palettes.uid) {
|
||||||
|
sopInstance.paletteColorLookupTableUID = palettes.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
sopInstance.redPaletteColorLookupTableData = palettes.red;
|
||||||
|
sopInstance.greenPaletteColorLookupTableData = palettes.green;
|
||||||
|
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
|
||||||
|
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
||||||
|
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
||||||
|
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
series.instances.push(sopInstance);
|
||||||
|
return sopInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a plain JS object that describes a study (a study descriptor object)
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {Object} aSopInstance a SOP Instance from which study information will be added
|
||||||
|
*/
|
||||||
|
function createStudy(server, aSopInstance) {
|
||||||
|
// TODO: Pass a reference ID to the server instead of including the URLs here
|
||||||
|
return {
|
||||||
|
seriesList: [],
|
||||||
|
seriesMap: Object.create(null),
|
||||||
|
seriesLoader: null,
|
||||||
|
wadoUriRoot: server.wadoUriRoot,
|
||||||
|
wadoRoot: server.wadoRoot,
|
||||||
|
qidoRoot: server.qidoRoot,
|
||||||
|
patientName: DICOMWeb.getName(aSopInstance['00100010']),
|
||||||
|
patientId: DICOMWeb.getString(aSopInstance['00100020']),
|
||||||
|
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
|
||||||
|
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
|
||||||
|
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
|
||||||
|
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
|
||||||
|
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
|
||||||
|
modalities: DICOMWeb.getString(aSopInstance['00080061']),
|
||||||
|
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
|
||||||
|
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
|
||||||
|
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
|
||||||
|
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list of SOP Instances to a given study object descriptor
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {Object} study The study descriptor to which the given SOP instances will be added
|
||||||
|
* @param {Array} sopInstanceList A list of SOP instance objects
|
||||||
|
*/
|
||||||
|
async function addInstancesToStudy(server, study, sopInstanceList) {
|
||||||
|
return Promise.all(
|
||||||
|
sopInstanceList.map(function(sopInstance) {
|
||||||
|
return makeSOPInstance(server, study, sopInstance);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses result data from a WADO search into Study MetaData
|
* Parses result data from a WADO search into Study MetaData
|
||||||
* Returns an object populated with study metadata, including the
|
* Returns an object populated with study metadata, including the
|
||||||
* series list.
|
* series list.
|
||||||
*
|
*
|
||||||
* @param server
|
* @param {Object} server Object with server configuration paramenters
|
||||||
* @param studyInstanceUid
|
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
|
||||||
* @param resultData
|
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
|
||||||
*/
|
*/
|
||||||
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
|
||||||
if (!resultData.length) {
|
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
|
||||||
return;
|
const firstSopInstance = sopInstanceList[0];
|
||||||
|
const study = createStudy(server, firstSopInstance);
|
||||||
|
await addInstancesToStudy(server, study, sopInstanceList);
|
||||||
|
return study;
|
||||||
}
|
}
|
||||||
|
throw new Error('Failed to create study out of provided SOP instance list');
|
||||||
const anInstance = resultData[0];
|
|
||||||
if (!anInstance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Pass a reference ID to the server instead of including the URLs here
|
|
||||||
const studyData = {
|
|
||||||
seriesList: [],
|
|
||||||
wadoUriRoot: server.wadoUriRoot,
|
|
||||||
wadoRoot: server.wadoRoot,
|
|
||||||
qidoRoot: server.qidoRoot,
|
|
||||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
|
||||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
|
||||||
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
|
|
||||||
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
|
|
||||||
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
|
|
||||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
|
||||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
|
||||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
|
||||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
|
||||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
|
||||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
|
||||||
institutionName: DICOMWeb.getString(anInstance['00080080']),
|
|
||||||
};
|
|
||||||
|
|
||||||
const seriesMap = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
resultData.map(async function(instance) {
|
|
||||||
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
|
||||||
let series = seriesMap[seriesInstanceUid];
|
|
||||||
|
|
||||||
if (!series) {
|
|
||||||
series = {
|
|
||||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
|
||||||
modality: DICOMWeb.getString(instance['00080060']),
|
|
||||||
seriesInstanceUid: seriesInstanceUid,
|
|
||||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
|
||||||
seriesDate: DICOMWeb.getString(instance['00080021']),
|
|
||||||
seriesTime: DICOMWeb.getString(instance['00080031']),
|
|
||||||
instances: [],
|
|
||||||
};
|
|
||||||
seriesMap[seriesInstanceUid] = series;
|
|
||||||
studyData.seriesList.push(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
|
||||||
const wadouri = buildInstanceWadoUrl(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
const baseWadoRsUri = buildInstanceWadoRsUri(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
const wadorsuri = buildInstanceFrameWadoRsUri(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
|
|
||||||
const instanceSummary = {
|
|
||||||
imageType: DICOMWeb.getString(instance['00080008']),
|
|
||||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
|
||||||
modality: DICOMWeb.getString(instance['00080060']),
|
|
||||||
sopInstanceUid,
|
|
||||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
|
||||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
|
||||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
|
||||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
|
||||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
|
||||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
|
||||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
|
||||||
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
|
||||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
|
||||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
|
||||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
|
||||||
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
|
||||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
|
||||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
|
||||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
|
||||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
|
||||||
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
|
||||||
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
|
||||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
|
||||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
|
||||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
|
||||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
|
||||||
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
|
||||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
|
||||||
laterality: DICOMWeb.getString(instance['00200062']),
|
|
||||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
|
||||||
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
|
||||||
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
|
||||||
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
|
||||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
|
||||||
frameTimeVector: parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00181065'])
|
|
||||||
),
|
|
||||||
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
|
||||||
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
|
||||||
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
|
||||||
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
|
||||||
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
|
||||||
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
|
||||||
echoNumber: DICOMWeb.getString(instance['00180086']),
|
|
||||||
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
|
||||||
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
|
||||||
baseWadoRsUri: baseWadoRsUri,
|
|
||||||
wadouri: WADOProxy.convertURL(wadouri, server),
|
|
||||||
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
|
||||||
wadoRoot: server.wadoRoot,
|
|
||||||
imageRendering: server.imageRendering,
|
|
||||||
thumbnailRendering: server.thumbnailRendering,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
|
||||||
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
|
|
||||||
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281101'])
|
|
||||||
);
|
|
||||||
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281102'])
|
|
||||||
);
|
|
||||||
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281103'])
|
|
||||||
);
|
|
||||||
const palettes = await getPaletteColors(
|
|
||||||
server,
|
|
||||||
instance,
|
|
||||||
redPaletteColorLookupTableDescriptor
|
|
||||||
);
|
|
||||||
|
|
||||||
if (palettes) {
|
|
||||||
if (palettes.uid) {
|
|
||||||
instanceSummary.paletteColorLookupTableUID = palettes.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceSummary.redPaletteColorLookupTableData = palettes.red;
|
|
||||||
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
|
|
||||||
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
|
|
||||||
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
|
||||||
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
|
||||||
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
series.instances.push(instanceSummary);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return studyData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve Study MetaData from a DICOM server using a WADO call
|
* Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
|
||||||
*
|
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed
|
||||||
* @param server
|
* @param {Object} server Object with server configuration paramenters
|
||||||
* @param studyInstanceUid
|
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
|
||||||
* @returns {Promise}
|
* @returns {Object} A study descriptor object
|
||||||
*/
|
*/
|
||||||
async function RetrieveMetadata(server, studyInstanceUid) {
|
async function RetrieveMetadata(server, studyInstanceUid) {
|
||||||
const config = {
|
return (server.enableStudyLazyLoad
|
||||||
|
? lazyLoadStudyMetadata
|
||||||
|
: loadStudyMetadata)(server, studyInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} server
|
||||||
|
* @param {*} studyInstanceUID
|
||||||
|
*/
|
||||||
|
async function loadStudyMetadata(server, studyInstanceUID) {
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
url: server.wadoRoot,
|
url: server.wadoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
};
|
});
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
return dicomWeb
|
||||||
const options = {
|
.retrieveStudyMetadata({ studyInstanceUID })
|
||||||
studyInstanceUID: studyInstanceUid,
|
.then(result => createStudyFromSOPInstanceList(server, result));
|
||||||
};
|
}
|
||||||
|
|
||||||
return dicomWeb.retrieveStudyMetadata(options).then(result => {
|
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
|
||||||
return resultDataToStudyMetadata(server, studyInstanceUid, result);
|
const seriesInstanceUids = await searchStudySeries(server, studyInstanceUid);
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
|
url: server.wadoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
const seriesLoader = makeSeriesLoader(
|
||||||
|
dicomWeb,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUids
|
||||||
|
);
|
||||||
|
const firstSeries = await seriesLoader.next();
|
||||||
|
const study = await createStudyFromSOPInstanceList(
|
||||||
|
server,
|
||||||
|
firstSeries.sopInstances
|
||||||
|
);
|
||||||
|
if (seriesLoader.hasNext()) {
|
||||||
|
attachSeriesLoader(server, study, seriesLoader);
|
||||||
|
}
|
||||||
|
return study;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachSeriesLoader(server, study, seriesLoader) {
|
||||||
|
study.seriesLoader = Object.freeze({
|
||||||
|
hasNext() {
|
||||||
|
return seriesLoader.hasNext();
|
||||||
|
},
|
||||||
|
async next() {
|
||||||
|
const series = await seriesLoader.next();
|
||||||
|
await addInstancesToStudy(server, study, series.sopInstances);
|
||||||
|
return study.seriesMap[series.seriesInstanceUID];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
||||||
|
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||||
|
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
|
||||||
|
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
|
||||||
|
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
|
||||||
|
*/
|
||||||
|
function makeSeriesLoader(
|
||||||
|
dicomWebClient,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUIDList
|
||||||
|
) {
|
||||||
|
return Object.freeze({
|
||||||
|
hasNext() {
|
||||||
|
return seriesInstanceUIDList.length > 0;
|
||||||
|
},
|
||||||
|
async next() {
|
||||||
|
const seriesInstanceUID = seriesInstanceUIDList.shift();
|
||||||
|
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
});
|
||||||
|
return { studyInstanceUID, seriesInstanceUID, sopInstances };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search series of a given study
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {string} studyInstanceUID The Study Instance UID to search series from;
|
||||||
|
* @returns {Arrays} A list of Series Instance UIDs
|
||||||
|
*/
|
||||||
|
async function searchStudySeries(server, studyInstanceUID) {
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
|
url: server.qidoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
const seriesList = await dicomWeb.searchForSeries({ studyInstanceUID });
|
||||||
|
return seriesList
|
||||||
|
.sort(seriesSortingCriteria)
|
||||||
|
.map(series => getSeriesInfo(series).seriesInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function seriesSortingCriteria(firstSeries, secondSeries) {
|
||||||
|
const a = getSeriesInfo(firstSeries);
|
||||||
|
const b = getSeriesInfo(secondSeries);
|
||||||
|
if (!a.isLowPriority && b.isLowPriority) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.isLowPriority && !b.isLowPriority) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.seriesNumber - b.seriesNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an object with processed series information and saves its reference
|
||||||
|
* inside the series object itself to simplify sorting
|
||||||
|
* @param {Object} series The raw series object
|
||||||
|
*/
|
||||||
|
function getSeriesInfo(series) {
|
||||||
|
let info = series[INFO];
|
||||||
|
if (!info) {
|
||||||
|
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
|
||||||
|
info = Object.freeze({
|
||||||
|
modality,
|
||||||
|
isLowPriority: isLowPriorityModality(modality),
|
||||||
|
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
|
||||||
|
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
|
||||||
|
});
|
||||||
|
series[INFO] = info;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
export default RetrieveMetadata;
|
export default RetrieveMetadata;
|
||||||
|
|||||||
5
platform/core/src/utils/isLowPriorityModality.js
Normal file
5
platform/core/src/utils/isLowPriorityModality.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR']);
|
||||||
|
|
||||||
|
export default function isLowPriorityModality(modality) {
|
||||||
|
return LOW_PRIORITY_MODALITIES.includes(modality);
|
||||||
|
}
|
||||||
@ -70,32 +70,48 @@ const getWadoRsInstanceMetaData = (study, series, instance) => {
|
|||||||
.toJSON();
|
.toJSON();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function updateMetaDataManager(study) {
|
/**
|
||||||
study.seriesList.forEach(series => {
|
* Update metadata manager with instances of a specifc series
|
||||||
series.instances.forEach(instance => {
|
* @param {Object} study A plain study descriptor object
|
||||||
// Cache just images that are going to be loaded via WADO-RS
|
* @param {Object} series A Series descriptor object contaning the instances to be added to the manager
|
||||||
if (
|
*/
|
||||||
instance.imageRendering !== 'wadors' &&
|
function updateMetaDataManagerForSeries(study, series) {
|
||||||
instance.thumbnailRendering !== 'wadors'
|
series.instances.forEach(instance => {
|
||||||
) {
|
// Cache just images that are going to be loaded via WADO-RS
|
||||||
return;
|
if (
|
||||||
}
|
instance.imageRendering !== 'wadors' &&
|
||||||
|
instance.thumbnailRendering !== 'wadors'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
||||||
const numberOfFrames = instance.numberOfFrames || 1;
|
const numberOfFrames = instance.numberOfFrames || 1;
|
||||||
|
|
||||||
// We can share the same metaData with all frames because it doesn't have
|
// We can share the same metaData with all frames because it doesn't have
|
||||||
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
||||||
// WADO-RS frame number is 1-based
|
// WADO-RS frame number is 1-based
|
||||||
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
||||||
const imageId = getWADORSImageId(instance, frameNumber);
|
const imageId = getWADORSImageId(instance, frameNumber);
|
||||||
|
|
||||||
// TODO Drop dependency on this
|
// TODO Drop dependency on this
|
||||||
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
|
cornerstoneWADOImageLoader.wadors.metaDataManager.add(imageId, metaData);
|
||||||
imageId,
|
}
|
||||||
metaData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update metadata manager
|
||||||
|
* @param {Object} study A plain study descriptor object
|
||||||
|
* @param {string} [seriesInstanceUid] The Series Instance UID of the series to be added (Optional)
|
||||||
|
*/
|
||||||
|
export default function updateMetaDataManager(study, seriesInstanceUid) {
|
||||||
|
if (seriesInstanceUid) {
|
||||||
|
const series = study.seriesMap[seriesInstanceUid];
|
||||||
|
updateMetaDataManagerForSeries(study, series);
|
||||||
|
} else {
|
||||||
|
study.seriesList.forEach(series => {
|
||||||
|
updateMetaDataManagerForSeries(study, series);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
/* global cornerstone */
|
||||||
import './ImageThumbnail.styl';
|
import './ImageThumbnail.styl';
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
@ -12,25 +13,6 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
|
|||||||
// - Pass in the entire load and render function as a prop?
|
// - Pass in the entire load and render function as a prop?
|
||||||
//import cornerstone from 'cornerstone-core';
|
//import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous wrapper around Cornerstone's renderToCanvas method.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} canvasElement An HTML <canvas> element
|
|
||||||
* @param {Image} image A Cornerstone Image
|
|
||||||
*
|
|
||||||
* @return {Promise} A promise tracking the progress of the rendering. Resolves empty.
|
|
||||||
*/
|
|
||||||
function renderAsync(canvasElement, image) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
cornerstone.renderToCanvas(canvasElement, image);
|
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ImageThumbnail extends PureComponent {
|
export default class ImageThumbnail extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
@ -52,40 +34,32 @@ export default class ImageThumbnail extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.canvas = React.createRef();
|
this.canvas = React.createRef();
|
||||||
|
|
||||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: renderIntoCanvas,
|
loading: this.shouldRenderToCanvas(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldRenderToCanvas() {
|
||||||
|
return this.props.imageId && !this.props.imageSrc;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
if (this.shouldRenderToCanvas()) {
|
||||||
|
cornerstone
|
||||||
if (renderIntoCanvas) {
|
.loadAndCacheImage(this.props.imageId)
|
||||||
const { imageId } = this.props;
|
.then(image => {
|
||||||
const canvas = this.canvas.current;
|
cornerstone.renderToCanvas(this.canvas.current, image);
|
||||||
|
this.setState({
|
||||||
cornerstone.loadAndCacheImage(imageId).then(
|
loading: false,
|
||||||
image => {
|
});
|
||||||
renderAsync(canvas, image).then(
|
})
|
||||||
() => {
|
.catch(error => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
error: true,
|
||||||
},
|
});
|
||||||
error => {
|
|
||||||
// TODO: Set state?
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
// TODO: Set state?
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,12 +74,10 @@ export default class ImageThumbnail extends PureComponent {
|
|||||||
const showStackLoadingProgressBar =
|
const showStackLoadingProgressBar =
|
||||||
this.props.stackPercentComplete !== undefined;
|
this.props.stackPercentComplete !== undefined;
|
||||||
|
|
||||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ImageThumbnail">
|
<div className="ImageThumbnail">
|
||||||
<div className="image-thumbnail-canvas">
|
<div className="image-thumbnail-canvas">
|
||||||
{renderIntoCanvas ? (
|
{this.shouldRenderToCanvas() ? (
|
||||||
<canvas
|
<canvas
|
||||||
ref={this.canvas}
|
ref={this.canvas}
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
@ -130,6 +102,9 @@ export default class ImageThumbnail extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{this.state.loading && (
|
||||||
|
<div className="image-thumbnail-loading-indicator"></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,36 @@
|
|||||||
-webkit-background-clip: padding
|
-webkit-background-clip: padding
|
||||||
background-clip: padding-box
|
background-clip: padding-box
|
||||||
|
|
||||||
|
// Thumbnail Loading Indicator
|
||||||
|
|
||||||
|
.image-thumbnail-loading-indicator
|
||||||
|
position: absolute
|
||||||
|
left: 50%
|
||||||
|
top: 50%
|
||||||
|
margin: -22px 0 0 -22px
|
||||||
|
display: block
|
||||||
|
width: 44px
|
||||||
|
height: 44px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: " "
|
||||||
|
display: block
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
margin: 1px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 5px solid var(--ui-border-color-dark)
|
||||||
|
border-color: var(--ui-border-color-dark) transparent var(--ui-border-color-dark) transparent
|
||||||
|
animation: image-thumbnail-loading-indicator 1.2s linear infinite
|
||||||
|
|
||||||
|
@keyframes image-thumbnail-loading-indicator
|
||||||
|
0%
|
||||||
|
transform: rotate(0deg)
|
||||||
|
100%
|
||||||
|
transform: rotate(360deg)
|
||||||
|
|
||||||
|
// The End...
|
||||||
|
|
||||||
.image-thumbnail-canvas
|
.image-thumbnail-canvas
|
||||||
height: 100%
|
height: 100%
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
@ -36,7 +66,6 @@
|
|||||||
-webkit-user-drag: none
|
-webkit-user-drag: none
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|
||||||
|
|
||||||
.thumbnailLoadingIndicator
|
.thumbnailLoadingIndicator
|
||||||
display: none
|
display: none
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|||||||
@ -25,7 +25,7 @@ class StudyBrowser extends Component {
|
|||||||
if (this.props.supportsDragAndDrop) {
|
if (this.props.supportsDragAndDrop) {
|
||||||
return (
|
return (
|
||||||
<ThumbnailEntryDragSource
|
<ThumbnailEntryDragSource
|
||||||
key={`${studyIndex}_${thumbIndex}`}
|
key={thumb.displaySetInstanceUid}
|
||||||
{...study}
|
{...study}
|
||||||
{...thumb}
|
{...thumb}
|
||||||
id={`${studyIndex}_${thumbIndex}`}
|
id={`${studyIndex}_${thumbIndex}`}
|
||||||
@ -37,7 +37,7 @@ class StudyBrowser extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className="ThumbnailEntryContainer">
|
<div className="ThumbnailEntryContainer">
|
||||||
<ThumbnailEntry
|
<ThumbnailEntry
|
||||||
key={`${studyIndex}_${thumbIndex}`}
|
key={thumb.displaySetInstanceUid}
|
||||||
{...study}
|
{...study}
|
||||||
{...thumb}
|
{...thumb}
|
||||||
id={`${studyIndex}_${thumbIndex}`}
|
id={`${studyIndex}_${thumbIndex}`}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
enableStudyLazyLoad: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,6 +12,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
enableStudyLazyLoad: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -11,6 +11,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
enableStudyLazyLoad: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
import { metadata, studies, utils } from "@ohif/core";
|
import { metadata, studies, utils, log } from '@ohif/core';
|
||||||
|
|
||||||
import ConnectedViewer from "./ConnectedViewer.js";
|
import ConnectedViewer from './ConnectedViewer.js';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import { extensionManager } from "./../App.js";
|
import { extensionManager } from './../App.js';
|
||||||
|
|
||||||
const { OHIFStudyMetadata } = metadata;
|
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
||||||
const { retrieveStudiesMetadata } = studies;
|
const { retrieveStudiesMetadata } = studies;
|
||||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
const { studyMetadataManager, updateMetaDataManager } = utils;
|
||||||
|
|
||||||
@ -13,65 +13,97 @@ class ViewerRetrieveStudyData extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
studyInstanceUids: PropTypes.array.isRequired,
|
studyInstanceUids: PropTypes.array.isRequired,
|
||||||
seriesInstanceUids: PropTypes.array,
|
seriesInstanceUids: PropTypes.array,
|
||||||
server: PropTypes.object
|
server: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
studies: null,
|
super(props);
|
||||||
error: null
|
this.state = {
|
||||||
};
|
studies: null,
|
||||||
|
error: null,
|
||||||
async componentDidMount() {
|
};
|
||||||
// TODO: Avoid using timepoints here
|
}
|
||||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
|
||||||
const { studyInstanceUids, seriesInstanceUids, server } = this.props;
|
|
||||||
|
|
||||||
|
async loadStudies() {
|
||||||
try {
|
try {
|
||||||
|
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
|
||||||
const studies = await retrieveStudiesMetadata(
|
const studies = await retrieveStudiesMetadata(
|
||||||
server,
|
server,
|
||||||
studyInstanceUids,
|
studyInstanceUids,
|
||||||
seriesInstanceUids
|
seriesInstanceUids
|
||||||
);
|
);
|
||||||
|
this.setStudies(studies);
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render the viewer when the data is ready
|
setStudies(givenStudies) {
|
||||||
// TODO: CLEAR THIS SOMEWHERE ELSE
|
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
|
||||||
studyMetadataManager.purge();
|
const sopClassHandlerModules =
|
||||||
|
extensionManager.modules['sopClassHandlerModule'];
|
||||||
// Map studies to new format, update metadata manager?
|
// Map studies to new format, update metadata manager?
|
||||||
const updatedStudies = studies.map(study => {
|
const studies = givenStudies.map(study => {
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
const studyMetadata = new OHIFStudyMetadata(
|
||||||
study,
|
study,
|
||||||
study.studyInstanceUid
|
study.studyInstanceUid
|
||||||
);
|
);
|
||||||
const sopClassHandlerModules =
|
if (!study.displaySets) {
|
||||||
extensionManager.modules["sopClassHandlerModule"];
|
study.displaySets = studyMetadata.createDisplaySets(
|
||||||
|
sopClassHandlerModules
|
||||||
study.displaySets =
|
);
|
||||||
study.displaySets ||
|
}
|
||||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
studyMetadata.setDisplaySets(study.displaySets);
|
||||||
|
|
||||||
// Updates WADO-RS metaDataManager
|
// Updates WADO-RS metaDataManager
|
||||||
updateMetaDataManager(study);
|
updateMetaDataManager(study);
|
||||||
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
studyMetadataManager.add(studyMetadata);
|
||||||
|
// Attempt to load remaning series if any
|
||||||
|
this._attemptToLoadRemainingSeries(studyMetadata);
|
||||||
return study;
|
return study;
|
||||||
});
|
});
|
||||||
|
this.setState({ studies });
|
||||||
this.setState({
|
|
||||||
studies: updatedStudies
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({
|
|
||||||
error: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Handle gracefully instead of throwing?
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addSeriesToStudy(studyMetadata, series) {
|
||||||
|
const sopClassHandlerModules =
|
||||||
|
extensionManager.modules['sopClassHandlerModule'];
|
||||||
|
const study = studyMetadata.getData();
|
||||||
|
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||||
|
studyMetadata.addSeries(seriesMetadata);
|
||||||
|
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
seriesMetadata
|
||||||
|
);
|
||||||
|
study.displaySets = studyMetadata.getDisplaySets();
|
||||||
|
updateMetaDataManager(study, series.seriesInstanceUid);
|
||||||
|
this.setState(function(state) {
|
||||||
|
return { studies: state.studies.slice() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_attemptToLoadRemainingSeries(studyMetadata) {
|
||||||
|
const { seriesLoader } = studyMetadata.getData();
|
||||||
|
if (!seriesLoader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (seriesLoader.hasNext()) {
|
||||||
|
seriesLoader
|
||||||
|
.next()
|
||||||
|
.then(
|
||||||
|
series => void this._addSeriesToStudy(studyMetadata, series),
|
||||||
|
error => void log.error(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// TODO: CLEAR THIS SOMEWHERE ELSE
|
||||||
|
studyMetadataManager.purge();
|
||||||
|
this.loadStudies();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user