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:
parent
9df57e7f7f
commit
55cf9f2306
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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 = {
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import HangingProtocolService from './HangingProtocolService';
|
||||||
|
|
||||||
|
export default HangingProtocolService;
|
||||||
@ -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'
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -23,6 +23,9 @@ export {
|
|||||||
ViewportGridContext,
|
ViewportGridContext,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
|
HangingProtocolContext,
|
||||||
|
HangingProtocolProvider,
|
||||||
|
useHangingProtocol,
|
||||||
} from './src/contextProviders';
|
} from './src/contextProviders';
|
||||||
|
|
||||||
/** COMPONENTS */
|
/** COMPONENTS */
|
||||||
|
|||||||
@ -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([
|
||||||
|
|||||||
@ -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']),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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?
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
153
platform/ui/src/contextProviders/HangingProtocolProvider.jsx
Normal file
153
platform/ui/src/contextProviders/HangingProtocolProvider.jsx
Normal 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);
|
||||||
@ -35,3 +35,9 @@ export {
|
|||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
} from './ViewportGridProvider';
|
} from './ViewportGridProvider';
|
||||||
|
|
||||||
|
export {
|
||||||
|
HangingProtocolContext,
|
||||||
|
HangingProtocolProvider,
|
||||||
|
useHangingProtocol,
|
||||||
|
} from './HangingProtocolProvider';
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user