From 55cf9f230659d2bc61f0fb5fcdc3e4936130c945 Mon Sep 17 00:00:00 2001 From: Erik Ziegler Date: Fri, 24 Jul 2020 12:51:57 +0200 Subject: [PATCH] OHIF-285/OHIF-274: Fix default hanging of display sets into the ViewportGrid (#1917) * OHIF-285/OHIF-274: Fix default hanging of display sets into the ViewportGrid Co-authored-by: James A. Petts --- .../default/src/DicomWebDataSource/index.js | 21 ++- .../wado/retrieveMetadata.js | 7 +- .../wado/retrieveMetadataLoaderAsync.js | 65 +++----- .../wado/utils/getSeriesInfo.js | 25 --- extensions/default/src/ViewerLayout/index.jsx | 37 ++++- .../ActionButtons.jsx | 4 +- .../PanelStudyBrowserTracking.jsx | 12 ++ platform/core/src/index.js | 3 + .../DicomMetadataStore/DicomMetadataStore.js | 20 ++- .../src/services/DicomMetadataStore/EVENTS.js | 1 + .../DicomMetadataStore/createStudyMetadata.js | 22 ++- .../HangingProtocolService.js | 74 +++++++++ .../services/HangingProtocolService/index.js | 3 + .../services/ToolBarService/ToolBarService.js | 1 - platform/core/src/services/index.js | 2 + platform/core/src/studies/getSeriesInfo.js | 25 --- platform/core/src/studies/index.js | 5 +- .../wado/retrieveMetadataLoaderAsync.js | 1 - platform/core/src/studies/sortStudy.js | 15 +- platform/core/src/utils/index.js | 3 + .../core/src/utils/isLowPriorityModality.js | 2 +- platform/ui/index.js | 3 + platform/ui/src/components/Button/Button.jsx | 2 +- .../components/ButtonGroup/ButtonGroup.jsx | 2 +- .../ui/src/components/SidePanel/SidePanel.jsx | 4 +- .../components/StudyBrowser/StudyBrowser.jsx | 2 +- .../ThumbnailList/ThumbnailList.jsx | 2 +- .../HangingProtocolProvider.jsx | 153 ++++++++++++++++++ platform/ui/src/contextProviders/index.js | 6 + platform/viewer/src/App.jsx | 22 +-- platform/viewer/src/appInit.js | 2 + .../viewer/src/components/ViewportGrid.jsx | 114 ++++++------- platform/viewer/src/routes/Mode/Mode.jsx | 96 ++++++++++- 33 files changed, 548 insertions(+), 208 deletions(-) delete mode 100644 extensions/default/src/DicomWebDataSource/wado/utils/getSeriesInfo.js create mode 100644 platform/core/src/services/HangingProtocolService/HangingProtocolService.js create mode 100644 platform/core/src/services/HangingProtocolService/index.js delete mode 100644 platform/core/src/studies/getSeriesInfo.js create mode 100644 platform/ui/src/contextProviders/HangingProtocolProvider.jsx diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index bfd4a8ffd..4fa32d5e5 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -42,7 +42,7 @@ function createDicomWebApi(dicomWebConfig) { wadoRoot, enableStudyLazyLoad, supportsFuzzyMatching, - supportsWildcard + supportsWildcard, } = dicomWebConfig; const qidoConfig = { @@ -63,9 +63,12 @@ function createDicomWebApi(dicomWebConfig) { query: { studies: { mapParams: mapParams.bind(), - search: async function (origParams) { + search: async function(origParams) { const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = - mapParams(origParams, { supportsFuzzyMatching, supportsWildcard }) || {}; + mapParams(origParams, { + supportsFuzzyMatching, + supportsWildcard, + }) || {}; const results = await qidoSearch( qidoDicomWebClient, @@ -80,7 +83,7 @@ function createDicomWebApi(dicomWebConfig) { }, series: { // mapParams: mapParams.bind(), - search: async function (studyInstanceUid) { + search: async function(studyInstanceUid) { const results = await seriesInStudy( qidoDicomWebClient, studyInstanceUid @@ -133,7 +136,8 @@ function createDicomWebApi(dicomWebConfig) { ); studyPromises.forEach(studyPromise => { - studyPromise.then(seriesPromises => { + studyPromise.then(data => { + const { seriesPromises } = data; seriesPromises.forEach(seriesPromise => { seriesPromise.then(instances => { storeInstances(instances); @@ -178,7 +182,10 @@ function createDicomWebApi(dicomWebConfig) { } // Get Series - const seriesPromises = await retrieveStudyMetadata( + const { + seriesSummaryMetadata, + seriesPromises, + } = await retrieveStudyMetadata( wadoDicomWebClient, StudyInstanceUID, enableStudyLazyLoad @@ -191,6 +198,8 @@ function createDicomWebApi(dicomWebConfig) { DicomMetadataStore.addInstances(naturalizedInstances); } + DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata); + seriesPromises.forEach(async seriesPromise => { const instances = await seriesPromise; storeInstances(instances); diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js index d0c8f9628..3b9ba2082 100644 --- a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadata.js @@ -29,9 +29,12 @@ async function RetrieveMetadata( studyInstanceUid, filters ); - const studyMetadata = retrieveMetadataLoader.execLoad(); + const { preLoadData, promises } = await retrieveMetadataLoader.execLoad(); - return studyMetadata; + return { + seriesSummaryMetadata: preLoadData, + seriesPromises: promises, + }; } export default RetrieveMetadata; diff --git a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js index 8e103850d..22ff600c8 100644 --- a/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js +++ b/extensions/default/src/DicomWebDataSource/wado/retrieveMetadataLoaderAsync.js @@ -1,15 +1,9 @@ +import dcmjs from 'dcmjs'; +import { studies } from '@ohif/core'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; -//import { sortStudySeries, sortingCriteria } from '../../sortStudy'; -import getSeriesInfo from './utils/getSeriesInfo'; -/** - * Map seriesList to an array of seriesInstanceUid - * @param {Arrays} seriesList list of Series Instance UIDs - * @returns {Arrays} A list of Series Instance UIDs - */ -function mapStudySeries(seriesList) { - return seriesList.map(series => getSeriesInfo(series).seriesInstanceUid); -} +const { sortStudySeries, sortingCriteria } = studies; + /** * Creates an immutable series loader object which loads each series sequentially using the iterator interface @@ -33,7 +27,6 @@ function makeSeriesAsyncLoader( studyInstanceUID, seriesInstanceUID, }); - // return { studyInstanceUID, seriesInstanceUID, sopInstances }; }, }); } @@ -73,56 +66,42 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader const preLoaders = this.getPreLoaders(); const result = await this.runLoaders(preLoaders); - // const seriesSorted = sortStudySeries( - // result, - // sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria - // ); + const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; + const naturalized = result.map(naturalizeDataset); - //const seriesInstanceUidsMap = mapStudySeries(seriesSorted); - - const seriesInstanceUidsMap = mapStudySeries(result); - - return seriesInstanceUidsMap; + return sortStudySeries( + naturalized, + sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria + ); } async load(preLoadData) { const { client, studyInstanceUID } = this; + const seriesInstanceUIDs = preLoadData.map(s => s.SeriesInstanceUID); + const seriesAsyncLoader = makeSeriesAsyncLoader( client, studyInstanceUID, - preLoadData + seriesInstanceUIDs ); - // const firstSeries = await seriesAsyncLoader.next(); - - // return { - // sopInstances: firstSeries.sopInstances, - // asyncLoader: seriesAsyncLoader, - // }; - const promises = []; while (seriesAsyncLoader.hasNext()) { promises.push(seriesAsyncLoader.next()); } - return promises; - - // if (asyncLoader.hasNext()) { + return { + preLoadData, + promises, + }; } - async posLoad(promises) { - return promises; - // const { client } = this; - // const { sopInstances, asyncLoader } = loadData; - - // const study = await createStudyFromSOPInstanceList(server, sopInstances); - - // if (asyncLoader.hasNext()) { - // attachSeriesLoader(server, study, asyncLoader); - // } - - // return study; + async posLoad({ preLoadData, promises }) { + return { + preLoadData, + promises, + }; } } diff --git a/extensions/default/src/DicomWebDataSource/wado/utils/getSeriesInfo.js b/extensions/default/src/DicomWebDataSource/wado/utils/getSeriesInfo.js deleted file mode 100644 index c59b573ea..000000000 --- a/extensions/default/src/DicomWebDataSource/wado/utils/getSeriesInfo.js +++ /dev/null @@ -1,25 +0,0 @@ -import { DICOMWeb } from '@ohif/core'; -// import isLowPriorityModality from '../utils/isLowPriorityModality'; - -const INFO = Symbol('INFO'); - -/** - * Creates an object with processed series information and saves its reference - * inside the series object itself. - * @param {Object} series The raw series object - * @returns {Object} object containing some useful info from given series - */ -export default function getSeriesInfo(series) { - let info = series[INFO]; - if (!info) { - const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase(); - info = Object.freeze({ - modality, - // isLowPriority: isLowPriorityModality(modality), // TODO -> We should do this in the series list level, not here. - seriesInstanceUid: DICOMWeb.getString(series['0020000E']), - seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0, - }); - series[INFO] = info; - } - return info; -} diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index cbf414efc..737dd25c2 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -54,7 +54,11 @@ function ViewerLayout({ }; }; - const defaultTool = { icon: 'tool-more-menu', label: 'More', isActive: false }; + const defaultTool = { + icon: 'tool-more-menu', + label: 'More', + isActive: false, + }; const [toolbars, setToolbars] = useState({ primary: [], secondary: [] }); const [activeTool, setActiveTool] = useState(defaultTool); @@ -63,7 +67,12 @@ function ViewerLayout({ }; const onPrimaryClickHandler = (evt, btn) => { - if (btn.props && btn.props.commands && evt.value && btn.props.commands[evt.value]) { + if ( + btn.props && + btn.props.commands && + evt.value && + btn.props.commands[evt.value] + ) { const { commandName, commandOptions } = btn.props.commands[evt.value]; commandsManager.runCommand(commandName, commandOptions); } @@ -75,8 +84,13 @@ function ViewerLayout({ () => { console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); const updatedToolbars = { - primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler, setActiveTool: setActiveToolHandler }), - secondary: ToolBarService.getButtonSection('secondary', { setActiveTool: setActiveToolHandler }), + primary: ToolBarService.getButtonSection('primary', { + onClick: onPrimaryClickHandler, + setActiveTool: setActiveToolHandler, + }), + secondary: ToolBarService.getButtonSection('secondary', { + setActiveTool: setActiveToolHandler, + }), }; setToolbars(updatedToolbars); } @@ -94,18 +108,25 @@ function ViewerLayout({
- {toolbars.primary.map(toolDef => { + {toolbars.primary.map((toolDef, index) => { const isNested = Array.isArray(toolDef); if (!isNested) { const { id, Component, componentProps } = toolDef; return ; } else { return ( - +
{toolDef.map(x => { const { id, Component, componentProps } = x; - return ; + return ( + + ); })}
@@ -174,7 +195,7 @@ ViewerLayout.propTypes = { leftPanels: PropTypes.array, rightPanels: PropTypes.array, /** Responsible for rendering our grid of viewports; provided by consuming application */ - children: PropTypes.oneOfType(PropTypes.node, PropTypes.func).isRequired, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, }; ViewerLayout.defaultProps = { diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx index bdd22bde0..93118b6a2 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/ActionButtons.jsx @@ -5,8 +5,8 @@ import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui'; function ActionButtons({ onExportClick, onCreateReportClick }) { return ( - - + + ds.StudyInstanceUID === study.studyInstanceUid ); + + displaySetsForStudy.sort((a,b) => { + if (a.seriesNumber !== b.seriesNumber) { + return a.seriesNumber - b.seriesNumber; + } + + const seriesDateA = Date.parse(a.seriesDate); + const seriesDateB = Date.parse(b.seriesDate); + + return seriesDateA - seriesDateB; + }); + const tabStudy = Object.assign({}, study, { displaySets: displaySetsForStudy, }); diff --git a/platform/core/src/index.js b/platform/core/src/index.js index a16604cc0..df4ce4d39 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -34,6 +34,7 @@ import { ToolBarSerivce, // TODO: Typo MeasurementService, ViewportGridService, + HangingProtocolService, } from './services'; import IWebApiDataSource from './DataSources/IWebApiDataSource'; @@ -80,6 +81,7 @@ const OHIF = { MeasurementService, ToolBarSerivce, // TODO: TYPO ViewportGridService, + HangingProtocolService, IWebApiDataSource, DicomMetadataStore, // @@ -123,6 +125,7 @@ export { MeasurementService, ToolBarSerivce, ViewportGridService, + HangingProtocolService, IWebApiDataSource, DicomMetadataStore, ViewModelProvider, diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 7768905fa..58eee20f3 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -72,7 +72,7 @@ const BaseImplementation = { study = _model.studies[_model.studies.length - 1]; } - study.addSeries(instances); + study.addInstancesToSeries(instances); // Broadcast an event even if we used cached data. // This is because the mode needs to listen to instances that are added to build up its active displaySets. @@ -83,6 +83,24 @@ const BaseImplementation = { SeriesInstanceUID, }); }, + addSeriesMetadata(seriesSummaryMetadata) { + const { StudyInstanceUID } = seriesSummaryMetadata[0]; + let study = _getStudy(StudyInstanceUID); + if (!study) { + study = createStudyMetadata(StudyInstanceUID); + _model.studies.push(study); + } + + seriesSummaryMetadata.forEach(series => { + const { SeriesInstanceUID } = series; + + study.setSeriesMetadata(SeriesInstanceUID, series); + }); + + this._broadcastEvent(EVENTS.SERIES_ADDED, { + StudyInstanceUID, + }); + }, addStudy(study) { const { StudyInstanceUID } = study; diff --git a/platform/core/src/services/DicomMetadataStore/EVENTS.js b/platform/core/src/services/DicomMetadataStore/EVENTS.js index e701c9011..7f30be511 100644 --- a/platform/core/src/services/DicomMetadataStore/EVENTS.js +++ b/platform/core/src/services/DicomMetadataStore/EVENTS.js @@ -1,5 +1,6 @@ const EVENTS = { INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded', + SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded', }; export default EVENTS; diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js index 3472215b6..8407e2d26 100644 --- a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js +++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js @@ -10,19 +10,31 @@ function createStudyMetadata(StudyInstanceUID) { * @param {string} instances[].SeriesInstanceUID * @returns {bool} true if series were added; false if series already exist */ - addSeries: function(instances) { + addInstancesToSeries: function(instances) { const { SeriesInstanceUID } = instances[0]; - const seriesExists = this.series.some( + const existingSeries = this.series.find( s => s.SeriesInstanceUID === SeriesInstanceUID ); - if (!seriesExists) { + if (existingSeries) { + existingSeries.instances.push(...instances); + } else { const series = createSeriesMetadata(instances); this.series.push(series); } - - return !seriesExists; }, + + setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) { + let existingSeries = this.series.find( + s => s.SeriesInstanceUID === SeriesInstanceUID + ); + + if (existingSeries) { + existingSeries = Object.assign(existingSeries, seriesMetadata); + } else { + this.series.push(Object.assign({ instances: [] }, seriesMetadata)); + } + } }; } diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js new file mode 100644 index 000000000..1127c0a66 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.js @@ -0,0 +1,74 @@ +const name = 'HangingProtocolService'; + +const publicAPI = { + name, + getState: _getState, + setHangingProtocol: _setHangingProtocol, + setHangingProtocolAppliedForViewport: _setHangingProtocolAppliedForViewport, + setServiceImplementation, + reset: _reset, + set: _set, +}; + +const serviceImplementation = { + _getState: () => console.warn('getState() NOT IMPLEMENTED'), + _setHangingProtocol: () => + console.warn('_setHangingProtocol() NOT IMPLEMENTED'), + _setHangingProtocolAppliedForViewport: () => + console.warn('_setHangingProtocolAppliedForViewport() NOT IMPLEMENTED'), + _reset: () => console.warn('reset() NOT IMPLEMENTED'), + _set: () => console.warn('set() NOT IMPLEMENTED'), +}; + +function _getState() { + return serviceImplementation._getState(); +} + +function _setHangingProtocol(hangingProtocol) { + return serviceImplementation._setHangingProtocol(hangingProtocol); +} + +function _setHangingProtocolAppliedForViewport(hpAlreadyApplied) { + return serviceImplementation._setHangingProtocolAppliedForViewport( + hpAlreadyApplied + ); +} + +function _set(state) { + return serviceImplementation._set(state); +} + +function _reset() { + return serviceImplementation._reset({}); +} + +function setServiceImplementation({ + getState: getStateImplementation, + setHangingProtocol: setHangingProtocolImplementation, + setHangingProtocolAppliedForViewport: setHangingProtocolAppliedForViewportImplementation, + reset: resetImplementation, + set: setImplementation, +}) { + if (getStateImplementation) { + serviceImplementation._getState = getStateImplementation; + } + if (setHangingProtocolImplementation) { + serviceImplementation._setHangingProtocol = setHangingProtocolImplementation; + } + if (setHangingProtocolAppliedForViewportImplementation) { + serviceImplementation._setHangingProtocolAppliedForViewport = setHangingProtocolAppliedForViewportImplementation; + } + if (resetImplementation) { + serviceImplementation._reset = resetImplementation; + } + if (setImplementation) { + serviceImplementation._set = setImplementation; + } +} + +export default { + name, + create: ({ configuration = {} }) => { + return publicAPI; + }, +}; diff --git a/platform/core/src/services/HangingProtocolService/index.js b/platform/core/src/services/HangingProtocolService/index.js new file mode 100644 index 000000000..2d5a41716 --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/index.js @@ -0,0 +1,3 @@ +import HangingProtocolService from './HangingProtocolService'; + +export default HangingProtocolService; diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index a9f7d34b4..c5c90d56b 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -42,7 +42,6 @@ export default class ToolBarService { } _buttonTypes() { - console.log(this.extensionManager.modules); const buttonTypes = {}; const registeredToolbarModules = this.extensionManager.modules[ 'toolbarModule' diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js index 3862a2554..7d119333d 100644 --- a/platform/core/src/services/index.js +++ b/platform/core/src/services/index.js @@ -8,6 +8,7 @@ import DicomMetadataStore from './DicomMetadataStore'; import DisplaySetService from './DisplaySetService'; import ToolBarSerivce from './ToolBarService'; import ViewportGridService from './ViewportGridService'; +import HangingProtocolService from './HangingProtocolService'; export { MeasurementService, @@ -20,4 +21,5 @@ export { DisplaySetService, ToolBarSerivce, ViewportGridService, + HangingProtocolService, }; diff --git a/platform/core/src/studies/getSeriesInfo.js b/platform/core/src/studies/getSeriesInfo.js deleted file mode 100644 index 347d9fa18..000000000 --- a/platform/core/src/studies/getSeriesInfo.js +++ /dev/null @@ -1,25 +0,0 @@ -import DICOMWeb from '../DICOMWeb/'; -import isLowPriorityModality from '../utils/isLowPriorityModality'; - -const INFO = Symbol('INFO'); - -/** - * Creates an object with processed series information and saves its reference - * inside the series object itself. - * @param {Object} series The raw series object - * @returns {Object} object containing some useful info from given series - */ -export default 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; -} diff --git a/platform/core/src/studies/index.js b/platform/core/src/studies/index.js index 86839da72..46eca2c83 100644 --- a/platform/core/src/studies/index.js +++ b/platform/core/src/studies/index.js @@ -7,7 +7,7 @@ import { import getStudyBoxData from './getStudyBoxData'; import retrieveStudiesMetadata from './retrieveStudiesMetadata.js'; import searchStudies from './searchStudies'; -import sortStudy from './sortStudy'; +import { sortStudy, sortStudySeries, sortStudyInstances, sortingCriteria } from './sortStudy'; const studies = { services: { @@ -21,6 +21,9 @@ const studies = { getStudyBoxData, searchStudies, sortStudy, + sortStudySeries, + sortStudyInstances, + sortingCriteria, }; export default studies; diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js b/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js index 81585772b..e285499c4 100644 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js +++ b/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js @@ -3,7 +3,6 @@ import dcmjs from 'dcmjs'; import DICOMWeb from '../../../DICOMWeb/'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; import { sortStudySeries, sortingCriteria } from '../../sortStudy'; -import getSeriesInfo from '../../getSeriesInfo'; import { createStudyFromSOPInstanceList, addInstancesToStudy, diff --git a/platform/core/src/studies/sortStudy.js b/platform/core/src/studies/sortStudy.js index aa0614dd9..5368c6d2a 100644 --- a/platform/core/src/studies/sortStudy.js +++ b/platform/core/src/studies/sortStudy.js @@ -1,4 +1,4 @@ -import getSeriesInfo from './getSeriesInfo'; +import isLowPriorityModality from '../utils/isLowPriorityModality'; /** * Series sorting criteria: series considered low priority are moved to the end @@ -7,15 +7,16 @@ import getSeriesInfo from './getSeriesInfo'; * @param {Object} secondSeries */ function seriesInfoSortingCriteria(firstSeries, secondSeries) { - const a = getSeriesInfo(firstSeries); - const b = getSeriesInfo(secondSeries); - if (!a.isLowPriority && b.isLowPriority) { + const aLowPriority = isLowPriorityModality(firstSeries.Modality); + const bLowPriority = isLowPriorityModality(secondSeries.Modality); + if (!aLowPriority && bLowPriority) { return -1; } - if (a.isLowPriority && !b.isLowPriority) { + if (aLowPriority && !bLowPriority) { return 1; } - return a.SeriesNumber - b.SeriesNumber; + + return firstSeries.SeriesNumber - secondSeries.SeriesNumber; } const seriesSortCriteria = { @@ -93,4 +94,4 @@ export default function sortStudy( return study; } -export { sortStudySeries, sortStudyInstances, sortingCriteria }; +export { sortStudy, sortStudySeries, sortStudyInstances, sortingCriteria }; diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index fdf6974cc..da42d9220 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -20,6 +20,7 @@ import formatPN from './formatPN'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; +import isLowPriorityModality from './isLowPriorityModality'; const utils = { guid, @@ -44,6 +45,7 @@ const utils = { resolveObjectPath, hierarchicalListUtils, progressTrackingUtils, + isLowPriorityModality }; export { @@ -68,6 +70,7 @@ export { resolveObjectPath, hierarchicalListUtils, progressTrackingUtils, + isLowPriorityModality }; export default utils; diff --git a/platform/core/src/utils/isLowPriorityModality.js b/platform/core/src/utils/isLowPriorityModality.js index d2794a8ce..54024d5a0 100644 --- a/platform/core/src/utils/isLowPriorityModality.js +++ b/platform/core/src/utils/isLowPriorityModality.js @@ -1,4 +1,4 @@ -const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR']); +const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR', 'SR', 'RTSTRUCT']); export default function isLowPriorityModality(Modality) { return LOW_PRIORITY_MODALITIES.includes(Modality); diff --git a/platform/ui/index.js b/platform/ui/index.js index bfb01724c..113057bff 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -23,6 +23,9 @@ export { ViewportGridContext, ViewportGridProvider, useViewportGrid, + HangingProtocolContext, + HangingProtocolProvider, + useHangingProtocol, } from './src/contextProviders'; /** COMPONENTS */ diff --git a/platform/ui/src/components/Button/Button.jsx b/platform/ui/src/components/Button/Button.jsx index fdcd13a0f..fdd5d9390 100644 --- a/platform/ui/src/components/Button/Button.jsx +++ b/platform/ui/src/components/Button/Button.jsx @@ -144,7 +144,7 @@ const Button = ({ Button.propTypes = { children: PropTypes.node, - size: PropTypes.oneOf(['small', 'medium', 'large', 'initial']), + size: PropTypes.oneOf(['small', 'medium', 'large', 'initial', 'inherit']), rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']), variant: PropTypes.oneOf(['text', 'outlined', 'contained']), color: PropTypes.oneOf([ diff --git a/platform/ui/src/components/ButtonGroup/ButtonGroup.jsx b/platform/ui/src/components/ButtonGroup/ButtonGroup.jsx index 78bfc3e27..101605738 100644 --- a/platform/ui/src/components/ButtonGroup/ButtonGroup.jsx +++ b/platform/ui/src/components/ButtonGroup/ButtonGroup.jsx @@ -135,7 +135,7 @@ ButtonGroup.propTypes = { fullWidth: PropTypes.bool, orientation: PropTypes.oneOf(['vertical', 'horizontal']), rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']), - size: PropTypes.oneOf(['small', 'medium', 'large']), + size: PropTypes.oneOf(['small', 'medium', 'large', 'inherit']), variant: PropTypes.oneOf(['text', 'outlined', 'contained']), }; diff --git a/platform/ui/src/components/SidePanel/SidePanel.jsx b/platform/ui/src/components/SidePanel/SidePanel.jsx index cc49896f8..452ebc46f 100644 --- a/platform/ui/src/components/SidePanel/SidePanel.jsx +++ b/platform/ui/src/components/SidePanel/SidePanel.jsx @@ -164,7 +164,7 @@ SidePanel.propTypes = { iconLabel: PropTypes.string.isRequired, name: PropTypes.string.isRequired, label: PropTypes.string.isRequired, - content: PropTypes.node, + content: PropTypes.func, // TODO: Should be node, but it keeps complaining? }), PropTypes.arrayOf( PropTypes.shape({ @@ -172,7 +172,7 @@ SidePanel.propTypes = { iconLabel: PropTypes.string.isRequired, name: PropTypes.string.isRequired, label: PropTypes.string.isRequired, - content: PropTypes.node, + content: PropTypes.func, // TODO: Should be node, but it keeps complaining? }) ), ]), diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx index 62763d063..c6899bbe2 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx @@ -143,7 +143,7 @@ StudyBrowser.propTypes = { 'thumbnailNoImage', ]).isRequired, isTracked: PropTypes.bool, - viewportIdentificator: PropTypes.string, + viewportIdentificator: PropTypes.arrayOf(PropTypes.string), /** * Data the thumbnail should expose to a receiving drop target. Use a matching * `dragData.type` to identify which targets can receive this draggable item. diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index d473beb5e..0f8de04d6 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -113,7 +113,7 @@ ThumbnailList.propTypes = { 'thumbnailTracked', 'thumbnailNoImage', ]).isRequired, - viewportIdentificator: PropTypes.string, + viewportIdentificator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), isTracked: PropTypes.bool, /** * Data the thumbnail should expose to a receiving drop target. Use a matching diff --git a/platform/ui/src/contextProviders/HangingProtocolProvider.jsx b/platform/ui/src/contextProviders/HangingProtocolProvider.jsx new file mode 100644 index 000000000..544ff95c5 --- /dev/null +++ b/platform/ui/src/contextProviders/HangingProtocolProvider.jsx @@ -0,0 +1,153 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useReducer, +} from 'react'; +import PropTypes from 'prop-types'; + +const DEFAULT_STATE = { + hangingProtocol: null, + hpAlreadyApplied: {}, +}; + +export const HangingProtocolContext = createContext(DEFAULT_STATE); + +export function HangingProtocolProvider({ children, service }) { + const hangingProtocolReducer = (state, action) => { + switch (action.type) { + case 'SET_HANGING_PROTOCOL': { + return { + ...state, + ...{ hangingProtocol: action.payload.hangingProtocol }, + }; + } + case 'SET_HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT': { + const index = action.payload.index; + const newHPAlreadyApplied = Object.assign({}, state.hpAlreadyApplied); + + newHPAlreadyApplied[index] = true; + + return { + ...state, + ...{ hpAlreadyApplied: newHPAlreadyApplied }, + }; + } + case 'RESET': { + return { + hangingProtocol: null, + hpAlreadyApplied: {}, + }; + } + + case 'SET': { + return { + ...state, + ...action.payload, + }; + } + + default: + return action.payload; + } + }; + + const [hangingProtocolState, dispatch] = useReducer( + hangingProtocolReducer, + DEFAULT_STATE + ); + + console.log('hangingProtocolState', hangingProtocolState); + + const getState = useCallback(() => hangingProtocolState, [ + hangingProtocolState, + ]); + + const setHangingProtocol = useCallback( + hangingProtocol => + dispatch({ + type: 'SET_HANGING_PROTOCOL', + payload: { + hangingProtocol, + }, + }), + [dispatch] + ); + + const setHangingProtocolAppliedForViewport = useCallback( + index => + dispatch({ + type: 'SET_HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT', + payload: { + index, + }, + }), + [dispatch] + ); + + const reset = useCallback( + () => + dispatch({ + type: 'RESET', + payload: {}, + }), + [dispatch] + ); + + const set = useCallback( + payload => + dispatch({ + type: 'SET', + payload, + }), + [dispatch] + ); + + /** + * Sets the implementation of the HangingProtocolService that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ + getState, + setHangingProtocol, + setHangingProtocolAppliedForViewport, + reset, + set, + }); + } + }, [ + getState, + service, + setHangingProtocol, + setHangingProtocolAppliedForViewport, + reset, + set, + ]); + + const api = { + // getState, + setHangingProtocol, + setHangingProtocolAppliedForViewport, + reset, + set, + }; + + return ( + + {children} + + ); +} + +HangingProtocolProvider.propTypes = { + children: PropTypes.any, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }).isRequired, +}; + +export const useHangingProtocol = () => useContext(HangingProtocolContext); diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index 6874f1a07..4a1e92908 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -35,3 +35,9 @@ export { ViewportGridProvider, useViewportGrid, } from './ViewportGridProvider'; + +export { + HangingProtocolContext, + HangingProtocolProvider, + useHangingProtocol, +} from './HangingProtocolProvider'; diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 1e72bd772..1c582d499 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -12,6 +12,7 @@ import { ThemeWrapper, ViewportDialogProvider, ViewportGridProvider, + HangingProtocolProvider, } from '@ohif/ui'; // Viewer Project // TODO: Should this influence study list? @@ -58,6 +59,7 @@ function App({ config, defaultExtensions }) { UINotificationService, UIViewportDialogService, ViewportGridService, // TODO: Should this be a "UI" Service? + HangingProtocolService, } = servicesManager.services; return ( @@ -66,15 +68,17 @@ function App({ config, defaultExtensions }) { - - - - - {appRoutes} - - - - + + + + + + {appRoutes} + + + + + diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index b986802fe..6e3a34662 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -11,6 +11,7 @@ import { DisplaySetService, ToolBarSerivce, ViewportGridService, + HangingProtocolService // utils, // redux as reduxOHIF, } from '@ohif/core'; @@ -54,6 +55,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { DisplaySetService, ToolBarSerivce, ViewportGridService, + HangingProtocolService ]); /** diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index 8de6dcc87..8d94dab3f 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -1,7 +1,7 @@ /** * CSS Grid Reference: http://grid.malven.co/ */ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useState } from 'react'; import PropTypes from 'prop-types'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import EmptyViewport from './EmptyViewport'; @@ -22,21 +22,53 @@ function ViewerViewportGrid(props) { } = viewportGrid; // TODO -> Need some way of selecting which displaySets hit the viewports. - const { DisplaySetService, MeasurementService } = servicesManager.services; + const { + DisplaySetService, + MeasurementService, + HangingProtocolService, + } = servicesManager.services; + // This is a placeholder for applying hanging protocols + // It probably shouldn't be done here + // For now it just hangs the first display set in the study in 1x1 + // as sorted by SeriesNumber useEffect(() => { const { unsubscribe } = DisplaySetService.subscribe( - DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, - displaySets => { - displaySets.sort((a, b) => { - const isImageSet = x => x instanceof ImageSet; - return isImageSet(a) === isImageSet(b) ? 0 : isImageSet(a) ? -1 : 1; - }); + DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, + eventData => { + const { displaySetsAdded } = eventData; - viewportGridService.setDisplaysetForViewport({ - viewportIndex: 0, - displaySetInstanceUID: displaySets[0].displaySetInstanceUID, - }); + const data = HangingProtocolService.getState(); + + // TODO: Sometimes this is undefined? + const { hpAlreadyApplied } = data; + + // Match each viewport individually + const numViewports = numRows * numCols; + for (let i = 0; i < numViewports; i++) { + if (hpAlreadyApplied[i] === true) { + return; + } + + // Temporary until matching is ported back over from the Meteor version. + const reqSeriesInstanceUID = + data.hangingProtocol.stages[0].viewports[0].seriesMatchingRules[0] + .constraint.equals.value; + const matchingDisplaySet = displaySetsAdded.find(ds => { + return ds.SeriesInstanceUID === reqSeriesInstanceUID; + }); + + if (!matchingDisplaySet) { + return; + } + + viewportGridService.setDisplaysetForViewport({ + viewportIndex: i, + displaySetInstanceUID: matchingDisplaySet.displaySetInstanceUID, + }); + + HangingProtocolService.setHangingProtocolAppliedForViewport(i); + } } ); @@ -70,32 +102,6 @@ function ViewerViewportGrid(props) { }; }, [viewports]); - // TODO -> Make a HangingProtocolService - // Commented out whilst not in use to avoid pointlessly regenerating this function. - // const HangingProtocolService = displaySets => { - // let displaySetInstanceUID; - - // // Fallback - // if (!displaySets || !displaySets.length) { - // const displaySet = DisplaySetService.activeDisplaySets[0]; - // displaySetInstanceUID = displaySet.displaySetInstanceUID; - // } else { - // const displaySet = displaySets[0]; - // displaySetInstanceUID = displaySet.displaySetInstanceUID; - // } - - // return { - // numRows: 1, - // numCols: 1, - // activeViewportIndex: 0, - // viewports: [ - // { - // displaySetInstanceUID, - // }, - // ], - // }; - // }; - const onDoubleClick = viewportIndex => { // TODO -> Disabled for now. // onNewImage on a cornerstone viewport is firing setDisplaySetForViewport. @@ -138,30 +144,6 @@ function ViewerViewportGrid(props) { }); }; - // TODO: - // Hmm... Should a "displaySet" being added update the viewport based on HP? - // I guess it might. - // This is where you would likely "fill" emptyViewports if none had content - // Or to recheck best placement/priority based on all activeDisplaySets - // useEffect(() => { - // const { unsubscribe } = DisplaySetService.subscribe( - // DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, - // displaySets => { - // displaySets.forEach(ds => console.log(`VPG:ADD::${ds.StudyInstanceUID}`)); - // const hp = HangingProtocolService(displaySets); - // viewportGrid.setViewportGrid(hp); - // }, - // ); - - // return unsubscribe; - // }, []); - - // const droppedDisplaySet = DisplaySetService.getDisplaySetByUID( - // displaySetInstanceUID - // ); - // const updatedViewportGridState = HangingProtocolService([ - // droppedDisplaySet, - // ]); const onDropHandler = (viewportIndex, { displaySetInstanceUID }) => { console.warn(`DROPPED: ${displaySetInstanceUID}`); viewportGridService.setDisplaysetForViewport({ @@ -188,7 +170,7 @@ function ViewerViewportGrid(props) { viewportComponents ); - const onInterationHandler = (event) => { + const onInterationHandler = event => { if (isActive) return; if (event) { @@ -211,7 +193,11 @@ function ViewerViewportGrid(props) { onInteraction={onInterationHandler} isActive={isActive} > -
+
{ // Core + // TODO: For some reason this is running before the Providers + // are calling setServiceImplementation // TOOD -> iterate through services. MeasurementService.clearMeasurements(); ViewportGridService.reset(); + HangingProtocolService.reset(); + // Extension extensionManager.onModeEnter(); // Mode @@ -150,6 +157,93 @@ export default function ModeRoute({ dataSource, ]); + useEffect(() => { + const { unsubscribe } = DicomMetadataStore.subscribe( + DicomMetadataStore.EVENTS.SERIES_ADDED, + ({ StudyInstanceUID }) => { + const studyMetadata = DicomMetadataStore.getStudy( + StudyInstanceUID, + ); + + const sortedSeries = studyMetadata.series.sort((a, b) => { + const aLowPriority = isLowPriorityModality(a.Modality); + const bLowPriority = isLowPriorityModality(b.Modality); + if (!aLowPriority && bLowPriority) { + return -1; + } + if (aLowPriority && !bLowPriority) { + return 1; + } + + return a.SeriesNumber - b.SeriesNumber; + }) + + const { SeriesInstanceUID } = sortedSeries[0] + + HangingProtocolService.setHangingProtocol( + { + /*protocolMatchingRules: [ + { + id: '7tmuq7KzDMCWFeapc', + weight: 2, + required: false, + attribute: 'x00081030', + constraint: { + contains: { + value: 'DFCI CT CHEST', + }, + }, + }, + ],*/ + stages: [ + { + /*id: 'v5PfGt9F6mffZPif5', + viewportStructure: { + type: 'grid', + properties: { + Rows: 1, + Columns: 1, + }, + layoutTemplateName: 'gridLayout', + },*/ + viewports: [ + { + viewportSettings: {}, + imageMatchingRules: [], + seriesMatchingRules: [ + { + id: 'mXnsCcNzZL56z7mTZ', + weight: 1, + required: true, + attribute: 'SeriesInstanceUID', + constraint: { + equals: { + value: SeriesInstanceUID, + }, + }, + }, + ], + studyMatchingRules: [], + }, + ], + } + ] + } + ) + } + ); + return unsubscribe; + }, [ + mode, + dataSourceName, + location, + DisplaySetService, + extensionManager, + sopClassHandlers, + StudyInstanceUIDsAsArray, + dataSource, + ]); + const reducer = (state, action) => { console.log(state, action); };