diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoader.js b/platform/core/src/studies/services/wado/retrieveMetadataLoader.js
index 087b6d1dc..6c695d3b9 100644
--- a/platform/core/src/studies/services/wado/retrieveMetadataLoader.js
+++ b/platform/core/src/studies/services/wado/retrieveMetadataLoader.js
@@ -40,7 +40,7 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
- } catch (e) {}
+ } catch (e) { }
}
if (loaders.next().done && !result) {
@@ -51,8 +51,8 @@ export default class RetrieveMetadataLoader {
}
// Methods to be overwrite
- async configLoad() {}
- async preLoad() {}
- async load(preLoadData) {}
- async posLoad(loadData) {}
+ async configLoad() { }
+ async preLoad() { }
+ async load(preLoadData) { }
+ async posLoad(loadData) { }
}
diff --git a/platform/core/src/studies/studyUtils.js b/platform/core/src/studies/studyUtils.js
new file mode 100644
index 000000000..a0dd54ffd
--- /dev/null
+++ b/platform/core/src/studies/studyUtils.js
@@ -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 };
diff --git a/platform/ui/src/components/studyBrowser/ImageThumbnail.js b/platform/ui/src/components/studyBrowser/ImageThumbnail.js
index 8f48b4545..90214ce3c 100644
--- a/platform/ui/src/components/studyBrowser/ImageThumbnail.js
+++ b/platform/ui/src/components/studyBrowser/ImageThumbnail.js
@@ -102,14 +102,14 @@ function ImageThumbnail(props) {
{shouldRenderToCanvas() ? (
) : (
-
- )}
+
+ )}
{loadingOrError}
{showStackLoadingProgressBar && (
diff --git a/platform/viewer/cypress/integration/common/OHIFStudyList.spec.js b/platform/viewer/cypress/integration/common/OHIFStudyList.spec.js
index 804f4ec32..ac80c6e20 100644
--- a/platform/viewer/cypress/integration/common/OHIFStudyList.spec.js
+++ b/platform/viewer/cypress/integration/common/OHIFStudyList.spec.js
@@ -1,14 +1,14 @@
//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.
-describe('OHIF Study List', function() {
- context('Desktop resolution', function() {
- beforeEach(function() {
+describe('OHIF Study List', function () {
+ context('Desktop resolution', function () {
+ beforeEach(function () {
cy.viewport(1750, 720);
cy.openStudyList();
cy.initStudyListAliasesOnDesktop();
});
- it('searches Patient Name with exact string', function() {
+ it('searches Patient Name with exact string', function () {
cy.get('@patientName').type('Juno');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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
const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function() {
// });
});
- context('Tablet resolution', function() {
- beforeEach(function() {
+ context('Tablet resolution', function () {
+ beforeEach(function () {
cy.viewport(1000, 660);
cy.openStudyList();
cy.initStudyListAliasesOnTablet();
});
- it('searches Patient Name with exact string', function() {
+ it('searches Patient Name with exact string', function () {
cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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');
//Wait result list to be displayed
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
const pageRows = [25, 50, 100];
diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js
index f77f2942b..37d807738 100644
--- a/platform/viewer/public/config/default.js
+++ b/platform/viewer/public/config/default.js
@@ -3,6 +3,7 @@ window.config = {
routerBasename: '/',
extensions: [],
showStudyList: true,
+ filterQueryParam: false,
servers: {
dicomWeb: [
{
diff --git a/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js
index 4ecb33438..f3a7e5ee2 100644
--- a/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js
+++ b/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js
@@ -1,6 +1,10 @@
import { connect } from 'react-redux';
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
+import OHIF from "@ohif/core";
+const {
+ clearViewportSpecificData
+} = OHIF.redux.actions;
const isActive = a => a.active === true;
const mapStateToProps = state => {
@@ -10,10 +14,17 @@ const mapStateToProps = state => {
server: activeServer,
};
};
+const mapDispatchToProps = dispatch => {
+ return {
+ clearViewportSpecificData: () => {
+ dispatch(clearViewportSpecificData());
+ }
+ };
+};
const ConnectedViewerRetrieveStudyData = connect(
mapStateToProps,
- null
+ mapDispatchToProps
)(ViewerRetrieveStudyData);
export default ConnectedViewerRetrieveStudyData;
diff --git a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js
index d898c3968..1b32e018a 100644
--- a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js
+++ b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js
@@ -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 ConnectedViewer from './ConnectedViewer.js';
import PropTypes from 'prop-types';
import { extensionManager } from './../App.js';
-import { withSnackbar } from '@ohif/ui';
+import { useSnackbarContext } from '@ohif/ui';
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
-const { studyMetadataManager, updateMetaDataManager } = utils;
+const { studyMetadataManager, updateMetaDataManager, makeCancelable } = utils;
-class ViewerRetrieveStudyData extends Component {
- static propTypes = {
- studyInstanceUids: PropTypes.array.isRequired,
- seriesInstanceUids: PropTypes.array,
- server: PropTypes.object,
+// Contexts
+import AppContext from '../context/AppContext';
+
+const _promoteToFront = (list, value, searchMethod) => {
+ 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);
- this.abortSeriesLoad = false;
- this.seriesLoadStats = Object.create(null);
- this.state = {
- studies: null,
- error: null,
- };
+/**
+ * Promote series to front if find found equivalent on filters object
+ * @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 _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() {
- try {
- const { server, studyInstanceUids, seriesInstanceUids } = this.props;
- const filters = {};
+ return promoted;
+};
- // Use the first, discard others
- const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
+const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
+ let promoted = false;
+ const queryParamsLength = Object.keys(filters).length;
+ const shouldPromoteToFront = queryParamsLength > 0;
- if (seriesInstanceUID) {
- 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 = {}) {
+ if (shouldPromoteToFront) {
const { seriesInstanceUID } = filters;
- const { snackbarContext } = this.props;
- // skip in case no filter or no toast manager
- if (!seriesInstanceUID || !snackbarContext) {
- return;
- }
+ const _seriesLookup = (valueToCompare, displaySet) => {
+ return displaySet.seriesInstanceUid === valueToCompare;
+ };
+ const promotedResponse = _promoteToFront(
+ studyMetadata.getDisplaySets(),
+ seriesInstanceUID,
+ _seriesLookup
+ );
- const firstStudy = studies[0] || {};
- const { seriesList = [] } = firstStudy;
- const firstSeries = seriesList[0];
-
- if (!firstSeries || firstSeries.seriesInstanceUid !== seriesInstanceUID) {
- snackbarContext.show({
- message: 'No series for given filter: ' + seriesInstanceUID,
- });
- }
+ study.displaySets = promotedResponse.data;
+ promoted = promotedResponse.promoted;
}
- setStudies(givenStudies) {
- if (Array.isArray(givenStudies) && givenStudies.length > 0) {
- const sopClassHandlerModules =
- extensionManager.modules['sopClassHandlerModule'];
+ return promoted;
+};
+
+/**
+ * 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?
- const studies = givenStudies.map(study => {
+ const studies = studiesData.map(study => {
const studyMetadata = new OHIFStudyMetadata(
study,
study.studyInstanceUid
);
- if (!study.displaySets) {
- study.displaySets = studyMetadata.createDisplaySets(
- sopClassHandlerModules
- );
- }
- studyMetadata.setDisplaySets(study.displaySets);
- // Updates WADO-RS metaDataManager
- updateMetaDataManager(study);
- studyMetadataManager.add(studyMetadata);
+
+ _updateStudyDisplaySets(study, studyMetadata);
+ _updateMetaDataManager(study, studyMetadata);
+
// 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;
});
- this.setState({ studies });
- }
- }
- _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() };
- });
- }
-
- _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;
+ setStudies(studies);
}
- this._addSeriesToStudy(studyMetadata, series);
- }
+ };
- _attemptToLoadRemainingSeries(studyMetadata) {
- const { seriesLoader } = studyMetadata.getData();
- if (!seriesLoader) {
- return;
- }
- 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++;
- }
- }
+ const loadStudies = async () => {
+ try {
+ const filters = {};
+ // Use the first, discard others
+ const seriesInstanceUID = seriesInstanceUids && seriesInstanceUids[0];
- componentWillUnmount() {
- this.abortSeriesLoad = true;
- for (const studyInstanceUid in this.seriesLoadStats) {
- const stats = this.seriesLoadStats[studyInstanceUid];
- if (stats && (stats.count > 0 || stats.errors > 0)) {
- deleteStudyMetadataPromise(studyInstanceUid);
- studyMetadataManager.remove(studyInstanceUid);
- log.info(`Purging incomplete study data: ${studyInstanceUid}`);
+ const retrieveParams = [server, studyInstanceUids];
+
+ if (seriesInstanceUID) {
+ filters.seriesInstanceUID = seriesInstanceUID;
+ // Query param filtering controlled by appConfig property
+ if (isFilterStrategy) {
+ retrieveParams.push(filters);
+ }
+ }
+
+ 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() {
- // TODO: CLEAR THIS SOMEWHERE ELSE
- studyMetadataManager.purge();
- this.loadStudies();
- }
-
- render() {
- if (this.state.error) {
- return