feat: Issue 879 viewer route query param not filtering but promoting (#1141)
* feat: 🎸 Filter by url query param for seriesInstnaceUID * fix: Set SR viewport as active by interaction (#1118) * fix: Set SR viewport as active by interaction * quick fix * (eslint) add "before" as global variables * add data-cy * add data-cy * create E2E test * (E2E) create custom command to set layout size * remove .only e2e * remove throttle for onScroll * feat: 🎸 Code review in progress Code review. Move retrieveMEtadata load to separate folders. Some minor code clean up * feat: 🎸 Code review. Missing changes from previous commit * feat: 🎸 Code review missing changes from previous commit When sorting, Criteria for instance must use instanceNumber and not instancesNumber * feat: 🎸 Code review. Add more jsdoc info * feat: 🎸 Code review. Prettify changed code * feat: 🎸 Instead of filtering do promote. WIP * feat: 🎸 Fix minor issue. Allow promote or filter * feat: 🎸 Changing component to functional component * Merge from master Part1/2 Conflicts solved: both added: core/src/studies/services/wado/retrieveMetadataLoader.js both added: core/src/studies/services/wado/retrieveMetadataLoaderAsync.js both added: core/src/studies/services/wado/retrieveMetadataLoaderSync.js both modified: viewer/src/connectedComponents/ViewerRetrieveStudyData.js both modified: viewer/src/routes/ViewerRouting.js * Merge process from master Part 1/2 Missing files from previous commit * feat: 🎸 Add cancelable promises to cut async methods * feat: 🎸 Missing changes from previous merge process * feat: 🎸 Missing changes from previous merge process
This commit is contained in:
parent
c77a2ef5c9
commit
b17f753e62
@ -40,7 +40,7 @@ export default class RetrieveMetadataLoader {
|
|||||||
if (result && result.length) {
|
if (result && result.length) {
|
||||||
break; // closes iterator in case data is retrieved successfully
|
break; // closes iterator in case data is retrieved successfully
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loaders.next().done && !result) {
|
if (loaders.next().done && !result) {
|
||||||
@ -51,8 +51,8 @@ export default class RetrieveMetadataLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Methods to be overwrite
|
// Methods to be overwrite
|
||||||
async configLoad() {}
|
async configLoad() { }
|
||||||
async preLoad() {}
|
async preLoad() { }
|
||||||
async load(preLoadData) {}
|
async load(preLoadData) { }
|
||||||
async posLoad(loadData) {}
|
async posLoad(loadData) { }
|
||||||
}
|
}
|
||||||
|
|||||||
413
platform/core/src/studies/studyUtils.js
Normal file
413
platform/core/src/studies/studyUtils.js
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
import DICOMWeb from '../DICOMWeb/';
|
||||||
|
|
||||||
|
const WADOProxy = {
|
||||||
|
convertURL: (url, server) => {
|
||||||
|
// TODO: Remove all WADOProxy stuff from this file
|
||||||
|
return url;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
function parseFloatArray(obj) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (!obj) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objs = obj.split('\\');
|
||||||
|
for (let i = 0; i < objs.length; i++) {
|
||||||
|
result.push(parseFloat(objs[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a plain JS object that describes a study (a study descriptor object)
|
||||||
|
* @param {Object} server Object with server configuration parameters
|
||||||
|
* @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']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a WADO url for an instance
|
||||||
|
*
|
||||||
|
* @param studyInstanceUid
|
||||||
|
* @param seriesInstanceUid
|
||||||
|
* @param sopInstanceUid
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function buildInstanceWadoUrl(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
) {
|
||||||
|
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
params.push('requestType=WADO');
|
||||||
|
params.push(`studyUID=${studyInstanceUid}`);
|
||||||
|
params.push(`seriesUID=${seriesInstanceUid}`);
|
||||||
|
params.push(`objectUID=${sopInstanceUid}`);
|
||||||
|
params.push('contentType=application/dicom');
|
||||||
|
params.push('transferSyntax=*');
|
||||||
|
|
||||||
|
const paramString = params.join('&');
|
||||||
|
|
||||||
|
return `${server.wadoUriRoot}?${paramString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInstanceWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
) {
|
||||||
|
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInstanceFrameWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid,
|
||||||
|
frame
|
||||||
|
) {
|
||||||
|
const baseWadoRsUri = buildInstanceWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
frame = frame != null || 1;
|
||||||
|
|
||||||
|
return `${baseWadoRsUri}/frames/${frame}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFrameIncrementPointer(element) {
|
||||||
|
const frameIncrementPointerNames = {
|
||||||
|
'00181065': 'frameTimeVector',
|
||||||
|
'00181063': 'frameTime',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!element || !element.Value || !element.Value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = element.Value[0];
|
||||||
|
return frameIncrementPointerNames[value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRadiopharmaceuticalInfo(instance) {
|
||||||
|
const modality = DICOMWeb.getString(instance['00080060']);
|
||||||
|
|
||||||
|
if (modality !== 'PT') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radiopharmaceuticalInfo = instance['00540016'];
|
||||||
|
if (
|
||||||
|
radiopharmaceuticalInfo === undefined ||
|
||||||
|
!radiopharmaceuticalInfo.Value ||
|
||||||
|
!radiopharmaceuticalInfo.Value.length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
|
||||||
|
return {
|
||||||
|
radiopharmaceuticalStartTime: DICOMWeb.getString(
|
||||||
|
firstPetRadiopharmaceuticalInfo['00181072']
|
||||||
|
),
|
||||||
|
radionuclideTotalDose: DICOMWeb.getNumber(
|
||||||
|
firstPetRadiopharmaceuticalInfo['00181074']
|
||||||
|
),
|
||||||
|
radionuclideHalfLife: DICOMWeb.getNumber(
|
||||||
|
firstPetRadiopharmaceuticalInfo['00181075']
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the SourceImageSequence, if it exists, in order
|
||||||
|
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||||
|
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||||
|
*
|
||||||
|
* @param instance
|
||||||
|
* @returns {String} The ReferenceSOPInstanceUID
|
||||||
|
*/
|
||||||
|
function getSourceImageInstanceUid(instance) {
|
||||||
|
// TODO= Parse the whole Source Image Sequence
|
||||||
|
// This is a really poor workaround for now.
|
||||||
|
// Later we should probably parse the whole sequence.
|
||||||
|
var SourceImageSequence = instance['00082112'];
|
||||||
|
if (
|
||||||
|
SourceImageSequence &&
|
||||||
|
SourceImageSequence.Value &&
|
||||||
|
SourceImageSequence.Value.length &&
|
||||||
|
SourceImageSequence.Value[0]['00081155'].Value
|
||||||
|
) {
|
||||||
|
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaletteColor(server, instance, tag, lutDescriptor) {
|
||||||
|
const numLutEntries = lutDescriptor[0];
|
||||||
|
const bits = lutDescriptor[2];
|
||||||
|
|
||||||
|
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
|
||||||
|
|
||||||
|
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
|
||||||
|
// incorrect bulk data URIs
|
||||||
|
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
|
||||||
|
uri = uri.replace('http', 'https');
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
};
|
||||||
|
const dicomWeb = new api.DICOMwebClient(config);
|
||||||
|
const options = {
|
||||||
|
BulkDataURI: uri,
|
||||||
|
};
|
||||||
|
|
||||||
|
const readUInt16 = (byteArray, position) => {
|
||||||
|
return byteArray[position] + byteArray[position + 1] * 256;
|
||||||
|
};
|
||||||
|
|
||||||
|
const arrayBufferToPaletteColorLUT = result => {
|
||||||
|
const arraybuffer = result[0];
|
||||||
|
const byteArray = new Uint8Array(arraybuffer);
|
||||||
|
const lut = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < numLutEntries; i++) {
|
||||||
|
if (bits === 16) {
|
||||||
|
lut[i] = readUInt16(byteArray, i * 2);
|
||||||
|
} else {
|
||||||
|
lut[i] = byteArray[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lut;
|
||||||
|
};
|
||||||
|
|
||||||
|
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
|
||||||
|
*
|
||||||
|
* @param server {Object} Current server;
|
||||||
|
* @param instance {Object} The retrieved instance metadata;
|
||||||
|
* @returns {String} The ReferenceSOPInstanceUID
|
||||||
|
*/
|
||||||
|
async function getPaletteColors(server, instance, lutDescriptor) {
|
||||||
|
let paletteUID = DICOMWeb.getString(instance['00281199']);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let entry;
|
||||||
|
if (paletteColorCache.isValidUID(paletteUID)) {
|
||||||
|
entry = paletteColorCache.get(paletteUID);
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
return resolve(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no entry in cache... Fetch remote data.
|
||||||
|
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
|
||||||
|
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
|
||||||
|
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
|
||||||
|
|
||||||
|
const promises = [r, g, b];
|
||||||
|
|
||||||
|
Promise.all(promises).then(args => {
|
||||||
|
entry = {
|
||||||
|
red: args[0],
|
||||||
|
green: args[1],
|
||||||
|
blue: args[2],
|
||||||
|
};
|
||||||
|
|
||||||
|
// when paletteUID is present, the entry can be cached...
|
||||||
|
entry.uid = paletteUID;
|
||||||
|
paletteColorCache.add(entry);
|
||||||
|
|
||||||
|
resolve(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list of SOP Instances to a given study object descriptor
|
||||||
|
* @param {Object} server Object with server configuration parameters
|
||||||
|
* @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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
|
||||||
|
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
|
||||||
|
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');
|
||||||
|
};
|
||||||
|
|
||||||
|
export { createStudyFromSOPInstanceList, addInstancesToStudy };
|
||||||
@ -102,14 +102,14 @@ function ImageThumbnail(props) {
|
|||||||
{shouldRenderToCanvas() ? (
|
{shouldRenderToCanvas() ? (
|
||||||
<canvas ref={canvasRef} width={width} height={height} />
|
<canvas ref={canvasRef} width={width} height={height} />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className="static-image"
|
className="static-image"
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
//width={this.props.width}
|
//width={this.props.width}
|
||||||
height={height}
|
height={height}
|
||||||
alt={''}
|
alt={''}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{loadingOrError}
|
{loadingOrError}
|
||||||
{showStackLoadingProgressBar && (
|
{showStackLoadingProgressBar && (
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
//We are keeping the hardcoded results values for the study list tests
|
//We are keeping the hardcoded results values for the study list tests
|
||||||
//this is intended to be running in a controled docker environment with test data.
|
//this is intended to be running in a controled docker environment with test data.
|
||||||
describe('OHIF Study List', function() {
|
describe('OHIF Study List', function () {
|
||||||
context('Desktop resolution', function() {
|
context('Desktop resolution', function () {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.viewport(1750, 720);
|
cy.viewport(1750, 720);
|
||||||
cy.openStudyList();
|
cy.openStudyList();
|
||||||
cy.initStudyListAliasesOnDesktop();
|
cy.initStudyListAliasesOnDesktop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Patient Name with exact string', function() {
|
it('searches Patient Name with exact string', function () {
|
||||||
cy.get('@patientName').type('Juno');
|
cy.get('@patientName').type('Juno');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -18,7 +18,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches MRN with exact string', function() {
|
it('searches MRN with exact string', function () {
|
||||||
cy.get('@MRN').type('ProstateX-0000');
|
cy.get('@MRN').type('ProstateX-0000');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -28,7 +28,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Accession with exact string', function() {
|
it('searches Accession with exact string', function () {
|
||||||
cy.get('@accessionNumber').type('fpcben98890');
|
cy.get('@accessionNumber').type('fpcben98890');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -38,7 +38,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Modality with camel case', function() {
|
it('searches Modality with camel case', function () {
|
||||||
cy.get('@modalities').type('Mr');
|
cy.get('@modalities').type('Mr');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -48,7 +48,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Description with exact string', function() {
|
it('searches Description with exact string', function () {
|
||||||
cy.get('@studyDescription').type('CHEST');
|
cy.get('@studyDescription').type('CHEST');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -58,7 +58,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('changes rows per page and checks the study count', function() {
|
it('changes rows per page and checks the study count', function () {
|
||||||
//Show rows per page options
|
//Show rows per page options
|
||||||
const pageRows = [25, 50, 100];
|
const pageRows = [25, 50, 100];
|
||||||
|
|
||||||
@ -100,14 +100,14 @@ describe('OHIF Study List', function() {
|
|||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Tablet resolution', function() {
|
context('Tablet resolution', function () {
|
||||||
beforeEach(function() {
|
beforeEach(function () {
|
||||||
cy.viewport(1000, 660);
|
cy.viewport(1000, 660);
|
||||||
cy.openStudyList();
|
cy.openStudyList();
|
||||||
cy.initStudyListAliasesOnTablet();
|
cy.initStudyListAliasesOnTablet();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Patient Name with exact string', function() {
|
it('searches Patient Name with exact string', function () {
|
||||||
cy.get('@patientNameOrMRN').type('Juno');
|
cy.get('@patientNameOrMRN').type('Juno');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -117,7 +117,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches MRN with with exact string', function() {
|
it('searches MRN with with exact string', function () {
|
||||||
cy.get('@patientNameOrMRN').type('ProstateX-0000');
|
cy.get('@patientNameOrMRN').type('ProstateX-0000');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -127,7 +127,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Modality with exact string', function() {
|
it('searches Modality with exact string', function () {
|
||||||
cy.get('@accessionModalityDescription').type('MR');
|
cy.get('@accessionModalityDescription').type('MR');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -137,7 +137,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Accession with exact string', function() {
|
it('searches Accession with exact string', function () {
|
||||||
cy.get('@accessionModalityDescription').type('fpcben98890');
|
cy.get('@accessionModalityDescription').type('fpcben98890');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -147,7 +147,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches Description with exact string', function() {
|
it('searches Description with exact string', function () {
|
||||||
cy.get('@accessionModalityDescription').type('CHEST');
|
cy.get('@accessionModalityDescription').type('CHEST');
|
||||||
//Wait result list to be displayed
|
//Wait result list to be displayed
|
||||||
cy.waitStudyList();
|
cy.waitStudyList();
|
||||||
@ -157,7 +157,7 @@ describe('OHIF Study List', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('changes rows per page and checks the study count', function() {
|
it('changes rows per page and checks the study count', function () {
|
||||||
//Show rows per page options
|
//Show rows per page options
|
||||||
const pageRows = [25, 50, 100];
|
const pageRows = [25, 50, 100];
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ window.config = {
|
|||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
extensions: [],
|
extensions: [],
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
|
filterQueryParam: false,
|
||||||
servers: {
|
servers: {
|
||||||
dicomWeb: [
|
dicomWeb: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
||||||
|
import OHIF from "@ohif/core";
|
||||||
|
|
||||||
|
const {
|
||||||
|
clearViewportSpecificData
|
||||||
|
} = OHIF.redux.actions;
|
||||||
const isActive = a => a.active === true;
|
const isActive = a => a.active === true;
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
@ -10,10 +14,17 @@ const mapStateToProps = state => {
|
|||||||
server: activeServer,
|
server: activeServer,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
clearViewportSpecificData: () => {
|
||||||
|
dispatch(clearViewportSpecificData());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const ConnectedViewerRetrieveStudyData = connect(
|
const ConnectedViewerRetrieveStudyData = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
null
|
mapDispatchToProps
|
||||||
)(ViewerRetrieveStudyData);
|
)(ViewerRetrieveStudyData);
|
||||||
|
|
||||||
export default ConnectedViewerRetrieveStudyData;
|
export default ConnectedViewerRetrieveStudyData;
|
||||||
|
|||||||
@ -1,191 +1,346 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
import { metadata, studies, utils, log } 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';
|
||||||
import { withSnackbar } from '@ohif/ui';
|
import { useSnackbarContext } from '@ohif/ui';
|
||||||
|
|
||||||
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
||||||
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
|
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
|
||||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
const { studyMetadataManager, updateMetaDataManager, makeCancelable } = utils;
|
||||||
|
|
||||||
class ViewerRetrieveStudyData extends Component {
|
// Contexts
|
||||||
static propTypes = {
|
import AppContext from '../context/AppContext';
|
||||||
studyInstanceUids: PropTypes.array.isRequired,
|
|
||||||
seriesInstanceUids: PropTypes.array,
|
const _promoteToFront = (list, value, searchMethod) => {
|
||||||
server: PropTypes.object,
|
let response = [...list];
|
||||||
|
let promoted = false;
|
||||||
|
const index = response.findIndex(searchMethod.bind(undefined, value));
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const first = response.splice(index, 1);
|
||||||
|
response = [...first, ...response];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
promoted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
promoted,
|
||||||
|
data: response,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
/**
|
||||||
super(props);
|
* Promote series to front if find found equivalent on filters object
|
||||||
this.abortSeriesLoad = false;
|
* @param {Object} study - study reference to promote series against
|
||||||
this.seriesLoadStats = Object.create(null);
|
* @param {Object} [filters] - Object containing filters to be applied
|
||||||
this.state = {
|
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||||
studies: null,
|
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
||||||
error: null,
|
*/
|
||||||
};
|
const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
|
||||||
|
let promoted = false;
|
||||||
|
// Promote only if no filter should be applied
|
||||||
|
if (!isFilterStrategy) {
|
||||||
|
_sortStudyDisplaySet(study, studyMetadata);
|
||||||
|
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadStudies() {
|
return promoted;
|
||||||
try {
|
};
|
||||||
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
|
|
||||||
const filters = {};
|
|
||||||
|
|
||||||
// Use the first, discard others
|
const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
|
||||||
const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
|
let promoted = false;
|
||||||
|
const queryParamsLength = Object.keys(filters).length;
|
||||||
|
const shouldPromoteToFront = queryParamsLength > 0;
|
||||||
|
|
||||||
if (seriesInstanceUID) {
|
if (shouldPromoteToFront) {
|
||||||
filters.seriesInstanceUID = seriesInstanceUID;
|
|
||||||
}
|
|
||||||
|
|
||||||
const studies = await retrieveStudiesMetadata(
|
|
||||||
server,
|
|
||||||
studyInstanceUids,
|
|
||||||
filters
|
|
||||||
);
|
|
||||||
this.validateFilters(studies, filters);
|
|
||||||
this.setStudies(studies);
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({ error: true });
|
|
||||||
log.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate filters and promp user a message in case filter is unsuccessfully applied.
|
|
||||||
* In case of success, studies array contains, as the first element, the queried content (from filter)
|
|
||||||
* @param {Array} studies array of studies to be evaluated
|
|
||||||
* @param {Object} filters filters to test against
|
|
||||||
*/
|
|
||||||
validateFilters(studies = [], filters = {}) {
|
|
||||||
const { seriesInstanceUID } = filters;
|
const { seriesInstanceUID } = filters;
|
||||||
|
|
||||||
const { snackbarContext } = this.props;
|
const _seriesLookup = (valueToCompare, displaySet) => {
|
||||||
// skip in case no filter or no toast manager
|
return displaySet.seriesInstanceUid === valueToCompare;
|
||||||
if (!seriesInstanceUID || !snackbarContext) {
|
};
|
||||||
return;
|
const promotedResponse = _promoteToFront(
|
||||||
}
|
studyMetadata.getDisplaySets(),
|
||||||
|
seriesInstanceUID,
|
||||||
|
_seriesLookup
|
||||||
|
);
|
||||||
|
|
||||||
const firstStudy = studies[0] || {};
|
study.displaySets = promotedResponse.data;
|
||||||
const { seriesList = [] } = firstStudy;
|
promoted = promotedResponse.promoted;
|
||||||
const firstSeries = seriesList[0];
|
|
||||||
|
|
||||||
if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
|
|
||||||
snackbarContext.show({
|
|
||||||
message: 'No series for given filter: ' + seriesInstanceUID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setStudies(givenStudies) {
|
return promoted;
|
||||||
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
|
};
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
/**
|
||||||
|
* Method to identify if query param (from url) was applied to given list
|
||||||
|
* @param {Object} study - study reference to promote series against
|
||||||
|
* @param {Object} [filters] - Object containing filters to be applied
|
||||||
|
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||||
|
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
||||||
|
*/
|
||||||
|
const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => {
|
||||||
|
const { seriesInstanceUID } = filters;
|
||||||
|
let applied = true;
|
||||||
|
// skip in case no filter or no toast manager
|
||||||
|
if (!seriesInstanceUID) {
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { seriesList = [], displaySets = [] } = study;
|
||||||
|
const firstSeries = isFilterStrategy ? seriesList[0] : displaySets[0];
|
||||||
|
|
||||||
|
if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
|
||||||
|
applied = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return applied;
|
||||||
|
};
|
||||||
|
const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
|
||||||
|
if (queryParamApplied) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { show: showUserMessage = () => { } } = dialog;
|
||||||
|
showUserMessage({
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _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,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
study.displaySets = studyMetadata.getDisplaySets();
|
||||||
|
_updateMetaDataManager(study, series.seriesInstanceUid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _updateMetaDataManager = (study, studyMetadata, series) => {
|
||||||
|
updateMetaDataManager(study, series);
|
||||||
|
|
||||||
|
const { studyInstanceUID } = study;
|
||||||
|
|
||||||
|
if (!studyMetadataManager.get(studyInstanceUID)) {
|
||||||
|
studyMetadataManager.add(studyMetadata);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _updateStudyDisplaySets = (study, studyMetadata) => {
|
||||||
|
const sopClassHandlerModules =
|
||||||
|
extensionManager.modules['sopClassHandlerModule'];
|
||||||
|
|
||||||
|
if (!study.displaySets) {
|
||||||
|
study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
studyMetadata.setDisplaySets(study.displaySets);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
||||||
|
studyMetadata.sortDisplaySets(study.displaySets);
|
||||||
|
};
|
||||||
|
const _loadRemainingSeries = studyMetadata => {
|
||||||
|
const { seriesLoader } = studyMetadata.getData();
|
||||||
|
if (!seriesLoader) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
const promisesLoaders = [];
|
||||||
|
while (seriesLoader.hasNext()) {
|
||||||
|
promisesLoaders.push(
|
||||||
|
seriesLoader
|
||||||
|
.next()
|
||||||
|
.then(
|
||||||
|
series => void _addSeriesToStudy(studyMetadata, series),
|
||||||
|
error => void log.error(error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promisesLoaders);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ViewerRetrieveStudyData({
|
||||||
|
server,
|
||||||
|
studyInstanceUids,
|
||||||
|
seriesInstanceUids,
|
||||||
|
clearViewportSpecificData,
|
||||||
|
}) {
|
||||||
|
// hooks
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [studies, setStudies] = useState([]);
|
||||||
|
const snackbarContext = useSnackbarContext();
|
||||||
|
const { appConfig = {} } = useContext(AppContext);
|
||||||
|
const { filterQueryParam: isFilterStrategy = false } = appConfig;
|
||||||
|
|
||||||
|
let cancelableSeriesPromises;
|
||||||
|
let cancelableStudiesPromises;
|
||||||
|
/**
|
||||||
|
* Callback method when study is totally loaded
|
||||||
|
* @param {object} study study loaded
|
||||||
|
* @param {object} studyMetadata studyMetadata for given study
|
||||||
|
* @param {Object} [filters] - Object containing filters to be applied
|
||||||
|
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||||
|
*/
|
||||||
|
const studyDidLoad = (study, studyMetadata, filters) => {
|
||||||
|
// User message
|
||||||
|
const promoted = _promoteList(
|
||||||
|
study,
|
||||||
|
studyMetadata,
|
||||||
|
filters,
|
||||||
|
isFilterStrategy
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear viewport to allow new promoted one to be displayed
|
||||||
|
if (promoted) {
|
||||||
|
clearViewportSpecificData(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isQueryParamApplied = _isQueryParamApplied(
|
||||||
|
study,
|
||||||
|
filters,
|
||||||
|
isFilterStrategy
|
||||||
|
);
|
||||||
|
// Show message in case not promoted neither filtered but should to
|
||||||
|
_showUserMessage(
|
||||||
|
isQueryParamApplied,
|
||||||
|
'Query parameters were not applied. Using original series list for given study.',
|
||||||
|
snackbarContext
|
||||||
|
);
|
||||||
|
|
||||||
|
setStudies([...studies, study]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to process studies. It will update displaySet, studyMetadata, load remaining series, ...
|
||||||
|
* @param {Array} studiesData Array of studies retrieved from server
|
||||||
|
* @param {Object} [filters] - Object containing filters to be applied
|
||||||
|
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
||||||
|
*/
|
||||||
|
const processStudies = (studiesData, filters) => {
|
||||||
|
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
||||||
// Map studies to new format, update metadata manager?
|
// Map studies to new format, update metadata manager?
|
||||||
const studies = givenStudies.map(study => {
|
const studies = studiesData.map(study => {
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
const studyMetadata = new OHIFStudyMetadata(
|
||||||
study,
|
study,
|
||||||
study.studyInstanceUid
|
study.studyInstanceUid
|
||||||
);
|
);
|
||||||
if (!study.displaySets) {
|
|
||||||
study.displaySets = studyMetadata.createDisplaySets(
|
_updateStudyDisplaySets(study, studyMetadata);
|
||||||
sopClassHandlerModules
|
_updateMetaDataManager(study, studyMetadata);
|
||||||
);
|
|
||||||
}
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
|
||||||
// Updates WADO-RS metaDataManager
|
|
||||||
updateMetaDataManager(study);
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
|
||||||
// Attempt to load remaning series if any
|
// Attempt to load remaning series if any
|
||||||
this._attemptToLoadRemainingSeries(studyMetadata);
|
cancelableSeriesPromises[study.studyInstanceUid] = makeCancelable(
|
||||||
|
_loadRemainingSeries(studyMetadata)
|
||||||
|
)
|
||||||
|
.then(result => {
|
||||||
|
if (result && !result.isCanceled) {
|
||||||
|
studyDidLoad(study, studyMetadata, filters);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && !error.isCanceled) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return study;
|
return study;
|
||||||
});
|
});
|
||||||
this.setState({ studies });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_addSeriesToStudy(studyMetadata, series) {
|
setStudies(studies);
|
||||||
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() };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleSeriesLoadResult(error, studyMetadata, series) {
|
|
||||||
if (this.abortSeriesLoad) return;
|
|
||||||
const stats = this.seriesLoadStats[studyMetadata.getStudyInstanceUID()];
|
|
||||||
if (!stats) return;
|
|
||||||
stats.count--;
|
|
||||||
if (error || !series) {
|
|
||||||
stats.errors++;
|
|
||||||
log.error(error || 'Bad Series');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this._addSeriesToStudy(studyMetadata, series);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
_attemptToLoadRemainingSeries(studyMetadata) {
|
const loadStudies = async () => {
|
||||||
const { seriesLoader } = studyMetadata.getData();
|
try {
|
||||||
if (!seriesLoader) {
|
const filters = {};
|
||||||
return;
|
// Use the first, discard others
|
||||||
}
|
const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
|
||||||
const stats = (this.seriesLoadStats[studyMetadata.getStudyInstanceUID()] = {
|
|
||||||
errors: 0,
|
|
||||||
count: 0,
|
|
||||||
});
|
|
||||||
while (seriesLoader.hasNext()) {
|
|
||||||
seriesLoader
|
|
||||||
.next()
|
|
||||||
.then(
|
|
||||||
series =>
|
|
||||||
void this._handleSeriesLoadResult(null, studyMetadata, series),
|
|
||||||
error => void this._handleSeriesLoadResult({ error }, null, null)
|
|
||||||
);
|
|
||||||
stats.count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
const retrieveParams = [server, studyInstanceUids];
|
||||||
this.abortSeriesLoad = true;
|
|
||||||
for (const studyInstanceUid in this.seriesLoadStats) {
|
if (seriesInstanceUID) {
|
||||||
const stats = this.seriesLoadStats[studyInstanceUid];
|
filters.seriesInstanceUID = seriesInstanceUID;
|
||||||
if (stats && (stats.count > 0 || stats.errors > 0)) {
|
// Query param filtering controlled by appConfig property
|
||||||
deleteStudyMetadataPromise(studyInstanceUid);
|
if (isFilterStrategy) {
|
||||||
studyMetadataManager.remove(studyInstanceUid);
|
retrieveParams.push(filters);
|
||||||
log.info(`Purging incomplete study data: ${studyInstanceUid}`);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelableStudiesPromises[studyInstanceUids] = makeCancelable(
|
||||||
|
retrieveStudiesMetadata(...retrieveParams)
|
||||||
|
)
|
||||||
|
.then(result => {
|
||||||
|
if (result && !result.isCanceled) {
|
||||||
|
processStudies(result, filters);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error && !error.isCanceled) {
|
||||||
|
setError(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error) {
|
||||||
|
setError(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
const purgeCancellablePromises = () => {
|
||||||
// TODO: CLEAR THIS SOMEWHERE ELSE
|
for (let studyInstanceUids in cancelableStudiesPromises) {
|
||||||
studyMetadataManager.purge();
|
if ('cancel' in cancelableStudiesPromises[studyInstanceUids]) {
|
||||||
this.loadStudies();
|
cancelableStudiesPromises[studyInstanceUids].cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.error) {
|
|
||||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
for (let studyInstanceUids in cancelableSeriesPromises) {
|
||||||
<ConnectedViewer
|
if ('cancel' in cancelableSeriesPromises[studyInstanceUids]) {
|
||||||
studies={this.state.studies}
|
cancelableSeriesPromises[studyInstanceUids].cancel();
|
||||||
studyInstanceUids={this.props.studyInstanceUids}
|
deleteStudyMetadataPromise(studyInstanceUids);
|
||||||
/>
|
studyMetadataManager.remove(studyInstanceUids);
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
studyMetadataManager.purge();
|
||||||
|
purgeCancellablePromises();
|
||||||
|
}, [studyInstanceUids]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cancelableSeriesPromises = {};
|
||||||
|
cancelableStudiesPromises = {};
|
||||||
|
loadStudies();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
purgeCancellablePromises();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div>Error: {JSON.stringify(error)}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConnectedViewer studies={studies} studyInstanceUids={studyInstanceUids} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withSnackbar(ViewerRetrieveStudyData);
|
ViewerRetrieveStudyData.propTypes = {
|
||||||
|
studyInstanceUids: PropTypes.array.isRequired,
|
||||||
|
seriesInstanceUids: PropTypes.array,
|
||||||
|
server: PropTypes.object,
|
||||||
|
clearViewportSpecificData: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewerRetrieveStudyData;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user