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 <jamesapetts@gmail.com>
This commit is contained in:
Erik Ziegler 2020-07-24 12:51:57 +02:00 committed by GitHub
parent 9df57e7f7f
commit 55cf9f2306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 548 additions and 208 deletions

View File

@ -42,7 +42,7 @@ function createDicomWebApi(dicomWebConfig) {
wadoRoot, wadoRoot,
enableStudyLazyLoad, enableStudyLazyLoad,
supportsFuzzyMatching, supportsFuzzyMatching,
supportsWildcard supportsWildcard,
} = dicomWebConfig; } = dicomWebConfig;
const qidoConfig = { const qidoConfig = {
@ -63,9 +63,12 @@ function createDicomWebApi(dicomWebConfig) {
query: { query: {
studies: { studies: {
mapParams: mapParams.bind(), mapParams: mapParams.bind(),
search: async function (origParams) { search: async function(origParams) {
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } = const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
mapParams(origParams, { supportsFuzzyMatching, supportsWildcard }) || {}; mapParams(origParams, {
supportsFuzzyMatching,
supportsWildcard,
}) || {};
const results = await qidoSearch( const results = await qidoSearch(
qidoDicomWebClient, qidoDicomWebClient,
@ -80,7 +83,7 @@ function createDicomWebApi(dicomWebConfig) {
}, },
series: { series: {
// mapParams: mapParams.bind(), // mapParams: mapParams.bind(),
search: async function (studyInstanceUid) { search: async function(studyInstanceUid) {
const results = await seriesInStudy( const results = await seriesInStudy(
qidoDicomWebClient, qidoDicomWebClient,
studyInstanceUid studyInstanceUid
@ -133,7 +136,8 @@ function createDicomWebApi(dicomWebConfig) {
); );
studyPromises.forEach(studyPromise => { studyPromises.forEach(studyPromise => {
studyPromise.then(seriesPromises => { studyPromise.then(data => {
const { seriesPromises } = data;
seriesPromises.forEach(seriesPromise => { seriesPromises.forEach(seriesPromise => {
seriesPromise.then(instances => { seriesPromise.then(instances => {
storeInstances(instances); storeInstances(instances);
@ -178,7 +182,10 @@ function createDicomWebApi(dicomWebConfig) {
} }
// Get Series // Get Series
const seriesPromises = await retrieveStudyMetadata( const {
seriesSummaryMetadata,
seriesPromises,
} = await retrieveStudyMetadata(
wadoDicomWebClient, wadoDicomWebClient,
StudyInstanceUID, StudyInstanceUID,
enableStudyLazyLoad enableStudyLazyLoad
@ -191,6 +198,8 @@ function createDicomWebApi(dicomWebConfig) {
DicomMetadataStore.addInstances(naturalizedInstances); DicomMetadataStore.addInstances(naturalizedInstances);
} }
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata);
seriesPromises.forEach(async seriesPromise => { seriesPromises.forEach(async seriesPromise => {
const instances = await seriesPromise; const instances = await seriesPromise;
storeInstances(instances); storeInstances(instances);

View File

@ -29,9 +29,12 @@ async function RetrieveMetadata(
studyInstanceUid, studyInstanceUid,
filters filters
); );
const studyMetadata = retrieveMetadataLoader.execLoad(); const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
return studyMetadata; return {
seriesSummaryMetadata: preLoadData,
seriesPromises: promises,
};
} }
export default RetrieveMetadata; export default RetrieveMetadata;

View File

@ -1,15 +1,9 @@
import dcmjs from 'dcmjs';
import { studies } from '@ohif/core';
import RetrieveMetadataLoader from './retrieveMetadataLoader'; import RetrieveMetadataLoader from './retrieveMetadataLoader';
//import { sortStudySeries, sortingCriteria } from '../../sortStudy';
import getSeriesInfo from './utils/getSeriesInfo';
/** const { sortStudySeries, sortingCriteria } = studies;
* 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);
}
/** /**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface * Creates an immutable series loader object which loads each series sequentially using the iterator interface
@ -33,7 +27,6 @@ function makeSeriesAsyncLoader(
studyInstanceUID, studyInstanceUID,
seriesInstanceUID, seriesInstanceUID,
}); });
// return { studyInstanceUID, seriesInstanceUID, sopInstances };
}, },
}); });
} }
@ -73,56 +66,42 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
const preLoaders = this.getPreLoaders(); const preLoaders = this.getPreLoaders();
const result = await this.runLoaders(preLoaders); const result = await this.runLoaders(preLoaders);
// const seriesSorted = sortStudySeries( const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
// result, const naturalized = result.map(naturalizeDataset);
// sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
// );
//const seriesInstanceUidsMap = mapStudySeries(seriesSorted); return sortStudySeries(
naturalized,
const seriesInstanceUidsMap = mapStudySeries(result); sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
);
return seriesInstanceUidsMap;
} }
async load(preLoadData) { async load(preLoadData) {
const { client, studyInstanceUID } = this; const { client, studyInstanceUID } = this;
const seriesInstanceUIDs = preLoadData.map(s => s.SeriesInstanceUID);
const seriesAsyncLoader = makeSeriesAsyncLoader( const seriesAsyncLoader = makeSeriesAsyncLoader(
client, client,
studyInstanceUID, studyInstanceUID,
preLoadData seriesInstanceUIDs
); );
// const firstSeries = await seriesAsyncLoader.next();
// return {
// sopInstances: firstSeries.sopInstances,
// asyncLoader: seriesAsyncLoader,
// };
const promises = []; const promises = [];
while (seriesAsyncLoader.hasNext()) { while (seriesAsyncLoader.hasNext()) {
promises.push(seriesAsyncLoader.next()); promises.push(seriesAsyncLoader.next());
} }
return promises; return {
preLoadData,
// if (asyncLoader.hasNext()) { promises,
};
} }
async posLoad(promises) { async posLoad({ preLoadData, promises }) {
return promises; return {
// const { client } = this; preLoadData,
// const { sopInstances, asyncLoader } = loadData; promises,
};
// const study = await createStudyFromSOPInstanceList(server, sopInstances);
// if (asyncLoader.hasNext()) {
// attachSeriesLoader(server, study, asyncLoader);
// }
// return study;
} }
} }

View File

@ -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;
}

View File

@ -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 [toolbars, setToolbars] = useState({ primary: [], secondary: [] });
const [activeTool, setActiveTool] = useState(defaultTool); const [activeTool, setActiveTool] = useState(defaultTool);
@ -63,7 +67,12 @@ function ViewerLayout({
}; };
const onPrimaryClickHandler = (evt, btn) => { 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]; const { commandName, commandOptions } = btn.props.commands[evt.value];
commandsManager.runCommand(commandName, commandOptions); commandsManager.runCommand(commandName, commandOptions);
} }
@ -75,8 +84,13 @@ function ViewerLayout({
() => { () => {
console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT'); console.warn('~~~ TOOL BAR MODIFIED EVENT CAUGHT');
const updatedToolbars = { const updatedToolbars = {
primary: ToolBarService.getButtonSection('primary', { onClick: onPrimaryClickHandler, setActiveTool: setActiveToolHandler }), primary: ToolBarService.getButtonSection('primary', {
secondary: ToolBarService.getButtonSection('secondary', { setActiveTool: setActiveToolHandler }), onClick: onPrimaryClickHandler,
setActiveTool: setActiveToolHandler,
}),
secondary: ToolBarService.getButtonSection('secondary', {
setActiveTool: setActiveToolHandler,
}),
}; };
setToolbars(updatedToolbars); setToolbars(updatedToolbars);
} }
@ -94,18 +108,25 @@ function ViewerLayout({
<Header> <Header>
<ErrorBoundary context="Primary Toolbar"> <ErrorBoundary context="Primary Toolbar">
<div className="relative flex justify-center"> <div className="relative flex justify-center">
{toolbars.primary.map(toolDef => { {toolbars.primary.map((toolDef, index) => {
const isNested = Array.isArray(toolDef); const isNested = Array.isArray(toolDef);
if (!isNested) { if (!isNested) {
const { id, Component, componentProps } = toolDef; const { id, Component, componentProps } = toolDef;
return <Component key={id} id={id} {...componentProps} />; return <Component key={id} id={id} {...componentProps} />;
} else { } else {
return ( return (
<NestedMenu isActive={activeTool.isActive} icon={activeTool.icon} label={activeTool.label}> <NestedMenu
key={index}
isActive={activeTool.isActive}
icon={activeTool.icon}
label={activeTool.label}
>
<div className="flex"> <div className="flex">
{toolDef.map(x => { {toolDef.map(x => {
const { id, Component, componentProps } = x; const { id, Component, componentProps } = x;
return <Component key={id} id={id} {...componentProps} />; return (
<Component key={id} id={id} {...componentProps} />
);
})} })}
</div> </div>
</NestedMenu> </NestedMenu>
@ -174,7 +195,7 @@ ViewerLayout.propTypes = {
leftPanels: PropTypes.array, leftPanels: PropTypes.array,
rightPanels: PropTypes.array, rightPanels: PropTypes.array,
/** Responsible for rendering our grid of viewports; provided by consuming application */ /** 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 = { ViewerLayout.defaultProps = {

View File

@ -5,8 +5,8 @@ import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
function ActionButtons({ onExportClick, onCreateReportClick }) { function ActionButtons({ onExportClick, onCreateReportClick }) {
return ( return (
<React.Fragment> <React.Fragment>
<ButtonGroup color="black" onClick={onExportClick} size="inherit"> <ButtonGroup color="black" size="inherit">
<Button className="text-base px-2 py-2">Export</Button> <Button className="text-base px-2 py-2" onClick={onExportClick}>Export</Button>
<IconButton <IconButton
className="px-2" className="px-2"
color="black" color="black"

View File

@ -447,6 +447,18 @@ function _createStudyBrowserTabs(
const displaySetsForStudy = displaySets.filter( const displaySetsForStudy = displaySets.filter(
ds => ds.StudyInstanceUID === study.studyInstanceUid ds => 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, { const tabStudy = Object.assign({}, study, {
displaySets: displaySetsForStudy, displaySets: displaySetsForStudy,
}); });

View File

@ -34,6 +34,7 @@ import {
ToolBarSerivce, // TODO: Typo ToolBarSerivce, // TODO: Typo
MeasurementService, MeasurementService,
ViewportGridService, ViewportGridService,
HangingProtocolService,
} from './services'; } from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource'; import IWebApiDataSource from './DataSources/IWebApiDataSource';
@ -80,6 +81,7 @@ const OHIF = {
MeasurementService, MeasurementService,
ToolBarSerivce, // TODO: TYPO ToolBarSerivce, // TODO: TYPO
ViewportGridService, ViewportGridService,
HangingProtocolService,
IWebApiDataSource, IWebApiDataSource,
DicomMetadataStore, DicomMetadataStore,
// //
@ -123,6 +125,7 @@ export {
MeasurementService, MeasurementService,
ToolBarSerivce, ToolBarSerivce,
ViewportGridService, ViewportGridService,
HangingProtocolService,
IWebApiDataSource, IWebApiDataSource,
DicomMetadataStore, DicomMetadataStore,
ViewModelProvider, ViewModelProvider,

View File

@ -72,7 +72,7 @@ const BaseImplementation = {
study = _model.studies[_model.studies.length - 1]; study = _model.studies[_model.studies.length - 1];
} }
study.addSeries(instances); study.addInstancesToSeries(instances);
// Broadcast an event even if we used cached data. // 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. // 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, 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) { addStudy(study) {
const { StudyInstanceUID } = study; const { StudyInstanceUID } = study;

View File

@ -1,5 +1,6 @@
const EVENTS = { const EVENTS = {
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded', INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
}; };
export default EVENTS; export default EVENTS;

View File

@ -10,19 +10,31 @@ function createStudyMetadata(StudyInstanceUID) {
* @param {string} instances[].SeriesInstanceUID * @param {string} instances[].SeriesInstanceUID
* @returns {bool} true if series were added; false if series already exist * @returns {bool} true if series were added; false if series already exist
*/ */
addSeries: function(instances) { addInstancesToSeries: function(instances) {
const { SeriesInstanceUID } = instances[0]; const { SeriesInstanceUID } = instances[0];
const seriesExists = this.series.some( const existingSeries = this.series.find(
s => s.SeriesInstanceUID === SeriesInstanceUID s => s.SeriesInstanceUID === SeriesInstanceUID
); );
if (!seriesExists) { if (existingSeries) {
existingSeries.instances.push(...instances);
} else {
const series = createSeriesMetadata(instances); const series = createSeriesMetadata(instances);
this.series.push(series); 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));
}
}
}; };
} }

View File

@ -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;
},
};

View File

@ -0,0 +1,3 @@
import HangingProtocolService from './HangingProtocolService';
export default HangingProtocolService;

View File

@ -42,7 +42,6 @@ export default class ToolBarService {
} }
_buttonTypes() { _buttonTypes() {
console.log(this.extensionManager.modules);
const buttonTypes = {}; const buttonTypes = {};
const registeredToolbarModules = this.extensionManager.modules[ const registeredToolbarModules = this.extensionManager.modules[
'toolbarModule' 'toolbarModule'

View File

@ -8,6 +8,7 @@ import DicomMetadataStore from './DicomMetadataStore';
import DisplaySetService from './DisplaySetService'; import DisplaySetService from './DisplaySetService';
import ToolBarSerivce from './ToolBarService'; import ToolBarSerivce from './ToolBarService';
import ViewportGridService from './ViewportGridService'; import ViewportGridService from './ViewportGridService';
import HangingProtocolService from './HangingProtocolService';
export { export {
MeasurementService, MeasurementService,
@ -20,4 +21,5 @@ export {
DisplaySetService, DisplaySetService,
ToolBarSerivce, ToolBarSerivce,
ViewportGridService, ViewportGridService,
HangingProtocolService,
}; };

View File

@ -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;
}

View File

@ -7,7 +7,7 @@ import {
import getStudyBoxData from './getStudyBoxData'; import getStudyBoxData from './getStudyBoxData';
import retrieveStudiesMetadata from './retrieveStudiesMetadata.js'; import retrieveStudiesMetadata from './retrieveStudiesMetadata.js';
import searchStudies from './searchStudies'; import searchStudies from './searchStudies';
import sortStudy from './sortStudy'; import { sortStudy, sortStudySeries, sortStudyInstances, sortingCriteria } from './sortStudy';
const studies = { const studies = {
services: { services: {
@ -21,6 +21,9 @@ const studies = {
getStudyBoxData, getStudyBoxData,
searchStudies, searchStudies,
sortStudy, sortStudy,
sortStudySeries,
sortStudyInstances,
sortingCriteria,
}; };
export default studies; export default studies;

View File

@ -3,7 +3,6 @@ import dcmjs from 'dcmjs';
import DICOMWeb from '../../../DICOMWeb/'; import DICOMWeb from '../../../DICOMWeb/';
import RetrieveMetadataLoader from './retrieveMetadataLoader'; import RetrieveMetadataLoader from './retrieveMetadataLoader';
import { sortStudySeries, sortingCriteria } from '../../sortStudy'; import { sortStudySeries, sortingCriteria } from '../../sortStudy';
import getSeriesInfo from '../../getSeriesInfo';
import { import {
createStudyFromSOPInstanceList, createStudyFromSOPInstanceList,
addInstancesToStudy, addInstancesToStudy,

View File

@ -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 * Series sorting criteria: series considered low priority are moved to the end
@ -7,15 +7,16 @@ import getSeriesInfo from './getSeriesInfo';
* @param {Object} secondSeries * @param {Object} secondSeries
*/ */
function seriesInfoSortingCriteria(firstSeries, secondSeries) { function seriesInfoSortingCriteria(firstSeries, secondSeries) {
const a = getSeriesInfo(firstSeries); const aLowPriority = isLowPriorityModality(firstSeries.Modality);
const b = getSeriesInfo(secondSeries); const bLowPriority = isLowPriorityModality(secondSeries.Modality);
if (!a.isLowPriority && b.isLowPriority) { if (!aLowPriority && bLowPriority) {
return -1; return -1;
} }
if (a.isLowPriority && !b.isLowPriority) { if (aLowPriority && !bLowPriority) {
return 1; return 1;
} }
return a.SeriesNumber - b.SeriesNumber;
return firstSeries.SeriesNumber - secondSeries.SeriesNumber;
} }
const seriesSortCriteria = { const seriesSortCriteria = {
@ -93,4 +94,4 @@ export default function sortStudy(
return study; return study;
} }
export { sortStudySeries, sortStudyInstances, sortingCriteria }; export { sortStudy, sortStudySeries, sortStudyInstances, sortingCriteria };

View File

@ -20,6 +20,7 @@ import formatPN from './formatPN';
import resolveObjectPath from './resolveObjectPath'; import resolveObjectPath from './resolveObjectPath';
import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as hierarchicalListUtils from './hierarchicalListUtils';
import * as progressTrackingUtils from './progressTrackingUtils'; import * as progressTrackingUtils from './progressTrackingUtils';
import isLowPriorityModality from './isLowPriorityModality';
const utils = { const utils = {
guid, guid,
@ -44,6 +45,7 @@ const utils = {
resolveObjectPath, resolveObjectPath,
hierarchicalListUtils, hierarchicalListUtils,
progressTrackingUtils, progressTrackingUtils,
isLowPriorityModality
}; };
export { export {
@ -68,6 +70,7 @@ export {
resolveObjectPath, resolveObjectPath,
hierarchicalListUtils, hierarchicalListUtils,
progressTrackingUtils, progressTrackingUtils,
isLowPriorityModality
}; };
export default utils; export default utils;

View File

@ -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) { export default function isLowPriorityModality(Modality) {
return LOW_PRIORITY_MODALITIES.includes(Modality); return LOW_PRIORITY_MODALITIES.includes(Modality);

View File

@ -23,6 +23,9 @@ export {
ViewportGridContext, ViewportGridContext,
ViewportGridProvider, ViewportGridProvider,
useViewportGrid, useViewportGrid,
HangingProtocolContext,
HangingProtocolProvider,
useHangingProtocol,
} from './src/contextProviders'; } from './src/contextProviders';
/** COMPONENTS */ /** COMPONENTS */

View File

@ -144,7 +144,7 @@ const Button = ({
Button.propTypes = { Button.propTypes = {
children: PropTypes.node, 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']), rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']),
variant: PropTypes.oneOf(['text', 'outlined', 'contained']), variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
color: PropTypes.oneOf([ color: PropTypes.oneOf([

View File

@ -135,7 +135,7 @@ ButtonGroup.propTypes = {
fullWidth: PropTypes.bool, fullWidth: PropTypes.bool,
orientation: PropTypes.oneOf(['vertical', 'horizontal']), orientation: PropTypes.oneOf(['vertical', 'horizontal']),
rounded: PropTypes.oneOf(['none', 'small', 'medium', 'large', 'full']), 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']), variant: PropTypes.oneOf(['text', 'outlined', 'contained']),
}; };

View File

@ -164,7 +164,7 @@ SidePanel.propTypes = {
iconLabel: PropTypes.string.isRequired, iconLabel: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
content: PropTypes.node, content: PropTypes.func, // TODO: Should be node, but it keeps complaining?
}), }),
PropTypes.arrayOf( PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
@ -172,7 +172,7 @@ SidePanel.propTypes = {
iconLabel: PropTypes.string.isRequired, iconLabel: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
content: PropTypes.node, content: PropTypes.func, // TODO: Should be node, but it keeps complaining?
}) })
), ),
]), ]),

View File

@ -143,7 +143,7 @@ StudyBrowser.propTypes = {
'thumbnailNoImage', 'thumbnailNoImage',
]).isRequired, ]).isRequired,
isTracked: PropTypes.bool, isTracked: PropTypes.bool,
viewportIdentificator: PropTypes.string, viewportIdentificator: PropTypes.arrayOf(PropTypes.string),
/** /**
* Data the thumbnail should expose to a receiving drop target. Use a matching * Data the thumbnail should expose to a receiving drop target. Use a matching
* `dragData.type` to identify which targets can receive this draggable item. * `dragData.type` to identify which targets can receive this draggable item.

View File

@ -113,7 +113,7 @@ ThumbnailList.propTypes = {
'thumbnailTracked', 'thumbnailTracked',
'thumbnailNoImage', 'thumbnailNoImage',
]).isRequired, ]).isRequired,
viewportIdentificator: PropTypes.string, viewportIdentificator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
isTracked: PropTypes.bool, isTracked: PropTypes.bool,
/** /**
* Data the thumbnail should expose to a receiving drop target. Use a matching * Data the thumbnail should expose to a receiving drop target. Use a matching

View File

@ -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 (
<HangingProtocolContext.Provider value={[hangingProtocolState, api]}>
{children}
</HangingProtocolContext.Provider>
);
}
HangingProtocolProvider.propTypes = {
children: PropTypes.any,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}).isRequired,
};
export const useHangingProtocol = () => useContext(HangingProtocolContext);

View File

@ -35,3 +35,9 @@ export {
ViewportGridProvider, ViewportGridProvider,
useViewportGrid, useViewportGrid,
} from './ViewportGridProvider'; } from './ViewportGridProvider';
export {
HangingProtocolContext,
HangingProtocolProvider,
useHangingProtocol,
} from './HangingProtocolProvider';

View File

@ -12,6 +12,7 @@ import {
ThemeWrapper, ThemeWrapper,
ViewportDialogProvider, ViewportDialogProvider,
ViewportGridProvider, ViewportGridProvider,
HangingProtocolProvider,
} from '@ohif/ui'; } from '@ohif/ui';
// Viewer Project // Viewer Project
// TODO: Should this influence study list? // TODO: Should this influence study list?
@ -58,6 +59,7 @@ function App({ config, defaultExtensions }) {
UINotificationService, UINotificationService,
UIViewportDialogService, UIViewportDialogService,
ViewportGridService, // TODO: Should this be a "UI" Service? ViewportGridService, // TODO: Should this be a "UI" Service?
HangingProtocolService,
} = servicesManager.services; } = servicesManager.services;
return ( return (
@ -66,6 +68,7 @@ function App({ config, defaultExtensions }) {
<Router basename={routerBasename}> <Router basename={routerBasename}>
<ThemeWrapper> <ThemeWrapper>
<ViewportGridProvider service={ViewportGridService}> <ViewportGridProvider service={ViewportGridService}>
<HangingProtocolProvider service={HangingProtocolService}>
<ViewportDialogProvider service={UIViewportDialogService}> <ViewportDialogProvider service={UIViewportDialogService}>
<SnackbarProvider service={UINotificationService}> <SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}> <DialogProvider service={UIDialogService}>
@ -75,6 +78,7 @@ function App({ config, defaultExtensions }) {
</DialogProvider> </DialogProvider>
</SnackbarProvider> </SnackbarProvider>
</ViewportDialogProvider> </ViewportDialogProvider>
</HangingProtocolProvider>
</ViewportGridProvider> </ViewportGridProvider>
</ThemeWrapper> </ThemeWrapper>
</Router> </Router>

View File

@ -11,6 +11,7 @@ import {
DisplaySetService, DisplaySetService,
ToolBarSerivce, ToolBarSerivce,
ViewportGridService, ViewportGridService,
HangingProtocolService
// utils, // utils,
// redux as reduxOHIF, // redux as reduxOHIF,
} from '@ohif/core'; } from '@ohif/core';
@ -54,6 +55,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
DisplaySetService, DisplaySetService,
ToolBarSerivce, ToolBarSerivce,
ViewportGridService, ViewportGridService,
HangingProtocolService
]); ]);
/** /**

View File

@ -1,7 +1,7 @@
/** /**
* CSS Grid Reference: http://grid.malven.co/ * 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 PropTypes from 'prop-types';
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
import EmptyViewport from './EmptyViewport'; import EmptyViewport from './EmptyViewport';
@ -22,21 +22,53 @@ function ViewerViewportGrid(props) {
} = viewportGrid; } = viewportGrid;
// TODO -> Need some way of selecting which displaySets hit the viewports. // 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(() => { useEffect(() => {
const { unsubscribe } = DisplaySetService.subscribe( const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED, DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
displaySets => { eventData => {
displaySets.sort((a, b) => { const { displaySetsAdded } = eventData;
const isImageSet = x => x instanceof ImageSet;
return isImageSet(a) === isImageSet(b) ? 0 : isImageSet(a) ? -1 : 1; 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({ viewportGridService.setDisplaysetForViewport({
viewportIndex: 0, viewportIndex: i,
displaySetInstanceUID: displaySets[0].displaySetInstanceUID, displaySetInstanceUID: matchingDisplaySet.displaySetInstanceUID,
}); });
HangingProtocolService.setHangingProtocolAppliedForViewport(i);
}
} }
); );
@ -70,32 +102,6 @@ function ViewerViewportGrid(props) {
}; };
}, [viewports]); }, [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 => { const onDoubleClick = viewportIndex => {
// TODO -> Disabled for now. // TODO -> Disabled for now.
// onNewImage on a cornerstone viewport is firing setDisplaySetForViewport. // 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 }) => { const onDropHandler = (viewportIndex, { displaySetInstanceUID }) => {
console.warn(`DROPPED: ${displaySetInstanceUID}`); console.warn(`DROPPED: ${displaySetInstanceUID}`);
viewportGridService.setDisplaysetForViewport({ viewportGridService.setDisplaysetForViewport({
@ -188,7 +170,7 @@ function ViewerViewportGrid(props) {
viewportComponents viewportComponents
); );
const onInterationHandler = (event) => { const onInterationHandler = event => {
if (isActive) return; if (isActive) return;
if (event) { if (event) {
@ -211,7 +193,11 @@ function ViewerViewportGrid(props) {
onInteraction={onInterationHandler} onInteraction={onInterationHandler}
isActive={isActive} isActive={isActive}
> >
<div className={classNames('h-full w-full flex flex-col align-center', { 'pointer-events-none': !isActive })}> <div
className={classNames('h-full w-full flex flex-col align-center', {
'pointer-events-none': !isActive,
})}
>
<ViewportComponent <ViewportComponent
displaySet={displaySet} displaySet={displaySet}
viewportIndex={viewportIndex} viewportIndex={viewportIndex}

View File

@ -2,13 +2,15 @@ import React, { useEffect } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected? // TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore } from '@ohif/core'; import { DicomMetadataStore, utils } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui'; import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
// //
import { useQuery } from '@hooks'; import { useQuery } from '@hooks';
import ViewportGrid from '@components/ViewportGrid'; import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose'; import Compose from './Compose';
const { isLowPriorityModality } = utils;
export default function ModeRoute({ export default function ModeRoute({
location, location,
mode, mode,
@ -46,6 +48,7 @@ export default function ModeRoute({
DisplaySetService, DisplaySetService,
MeasurementService, MeasurementService,
ViewportGridService, ViewportGridService,
HangingProtocolService,
} = servicesManager.services; } = servicesManager.services;
const layoutTemplateData = route.layoutTemplate({ location }); const layoutTemplateData = route.layoutTemplate({ location });
@ -98,9 +101,13 @@ export default function ModeRoute({
useEffect(() => { useEffect(() => {
// Core // Core
// TODO: For some reason this is running before the Providers
// are calling setServiceImplementation
// TOOD -> iterate through services. // TOOD -> iterate through services.
MeasurementService.clearMeasurements(); MeasurementService.clearMeasurements();
ViewportGridService.reset(); ViewportGridService.reset();
HangingProtocolService.reset();
// Extension // Extension
extensionManager.onModeEnter(); extensionManager.onModeEnter();
// Mode // Mode
@ -150,6 +157,93 @@ export default function ModeRoute({
dataSource, 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) => { const reducer = (state, action) => {
console.log(state, action); console.log(state, action);
}; };