Files
ohif-viewer/platform/viewer/src/connectedComponents/Viewer.js
T

653 lines
21 KiB
JavaScript
Raw Normal View History

import React, { Component } from 'react';
import PropTypes from 'prop-types';
2019-07-04 14:09:13 -04:00
import classNames from 'classnames';
import { useLogger } from '@ohif/ui';
2020-06-04 06:52:30 -03:00
import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core';
import { withDialog } from '@ohif/ui';
import moment from 'moment';
import ConnectedHeader from './ConnectedHeader.js';
2020-05-06 14:17:44 -03:00
import ToolbarRow from './ToolbarRow.js';
2019-07-04 14:09:13 -04:00
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
2020-06-04 06:52:30 -03:00
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
2019-07-04 14:09:13 -04:00
import { extensionManager } from './../App.js';
import { ReconstructionIssues } from './../../../core/src/enums.js';
import dcmjs from 'dcmjs';
// Contexts
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
import UserManagerContext from '../context/UserManagerContext';
2020-04-17 12:26:09 -04:00
import AppContext from '../context/AppContext';
import './Viewer.css';
import { finished } from 'stream';
import { cornerstoneWADOImageLoader } from 'cornerstone-wado-image-loader';
2018-12-14 15:23:48 +01:00
class Viewer extends Component {
static propTypes = {
2020-02-21 16:09:05 -05:00
studies: PropTypes.arrayOf(
PropTypes.shape({
StudyInstanceUID: PropTypes.string.isRequired,
StudyDate: PropTypes.string,
2020-06-04 06:52:30 -03:00
PatientID: PropTypes.string,
2020-02-21 16:09:05 -05:00
displaySets: PropTypes.arrayOf(
PropTypes.shape({
displaySetInstanceUID: PropTypes.string.isRequired,
SeriesDescription: PropTypes.string,
SeriesNumber: PropTypes.number,
InstanceNumber: PropTypes.number,
2020-02-21 16:09:05 -05:00
numImageFrames: PropTypes.number,
Modality: PropTypes.string.isRequired,
2020-02-21 16:09:05 -05:00
images: PropTypes.arrayOf(
PropTypes.shape({
getImageId: PropTypes.func.isRequired,
})
),
})
),
})
),
studyInstanceUIDs: PropTypes.array,
2019-12-11 13:14:01 -03:00
activeServer: PropTypes.shape({
type: PropTypes.string,
wadoRoot: PropTypes.string,
}),
onTimepointsUpdated: PropTypes.func,
2019-05-29 08:39:54 -04:00
onMeasurementsUpdated: PropTypes.func,
2019-07-04 14:09:13 -04:00
// window.store.getState().viewports.viewportSpecificData
viewports: PropTypes.object.isRequired,
// window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired,
2019-12-11 13:14:01 -03:00
isStudyLoaded: PropTypes.bool,
dialog: PropTypes.object,
};
constructor(props) {
super(props);
2019-12-11 13:14:01 -03:00
const { activeServer } = this.props;
const server = Object.assign({}, activeServer);
OHIF.measurements.MeasurementApi.setConfiguration({
dataExchange: {
2019-12-11 13:14:01 -03:00
retrieve: DICOMSR.retrieveMeasurements,
store: DICOMSR.storeMeasurements,
2019-05-29 08:39:54 -04:00
},
2019-12-11 13:14:01 -03:00
server,
});
OHIF.measurements.TimepointApi.setConfiguration({
dataExchange: {
retrieve: this.retrieveTimepoints,
store: this.storeTimepoints,
remove: this.removeTimepoint,
update: this.updateTimepoint,
2019-05-29 08:39:54 -04:00
disassociate: this.disassociateStudy,
},
});
this._getActiveViewport = this._getActiveViewport.bind(this);
}
2019-07-04 14:09:13 -04:00
state = {
isLeftSidePanelOpen: true,
isRightSidePanelOpen: false,
selectedRightSidePanel: '',
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
thumbnails: [],
};
componentWillUnmount() {
if (this.props.dialog) {
this.props.dialog.dismissAll();
}
}
retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints');
// Get the earliest and latest study date
let earliestDate = new Date().toISOString();
let latestDate = new Date().toISOString();
if (this.props.studies) {
latestDate = new Date('1000-01-01').toISOString();
this.props.studies.forEach(study => {
const StudyDate = moment(study.StudyDate, 'YYYYMMDD').toISOString();
if (StudyDate < earliestDate) {
earliestDate = StudyDate;
}
if (StudyDate > latestDate) {
latestDate = StudyDate;
}
});
}
// Return a generic timepoint
return Promise.resolve([
{
timepointType: 'baseline',
timepointId: 'TimepointId',
studyInstanceUIDs: this.props.studyInstanceUIDs,
PatientID: filter.PatientID,
earliestDate,
latestDate,
2019-05-29 08:39:54 -04:00
isLocked: false,
},
]);
};
storeTimepoints = timepointData => {
OHIF.log.info('storeTimepoints');
return Promise.resolve();
};
updateTimepoint = (timepointData, query) => {
OHIF.log.info('updateTimepoint');
return Promise.resolve();
};
removeTimepoint = timepointId => {
OHIF.log.info('removeTimepoint');
return Promise.resolve();
};
disassociateStudy = (timepointIds, StudyInstanceUID) => {
OHIF.log.info('disassociateStudy');
return Promise.resolve();
};
onTimepointsUpdated = timepoints => {
if (this.props.onTimepointsUpdated) {
this.props.onTimepointsUpdated(timepoints);
}
};
onMeasurementsUpdated = measurements => {
if (this.props.onMeasurementsUpdated) {
this.props.onMeasurementsUpdated(measurements);
}
};
componentDidMount() {
2019-12-11 13:14:01 -03:00
const { studies, isStudyLoaded } = this.props;
const { TimepointApi, MeasurementApi } = OHIF.measurements;
const currentTimepointId = 'TimepointId';
const timepointApi = new TimepointApi(currentTimepointId, {
2019-05-29 08:39:54 -04:00
onTimepointsUpdated: this.onTimepointsUpdated,
});
const measurementApi = new MeasurementApi(timepointApi, {
2019-05-29 08:39:54 -04:00
onMeasurementsUpdated: this.onMeasurementsUpdated,
});
this.currentTimepointId = currentTimepointId;
this.timepointApi = timepointApi;
this.measurementApi = measurementApi;
if (studies) {
const PatientID = studies[0] && studies[0].PatientID;
2019-07-04 14:09:13 -04:00
timepointApi.retrieveTimepoints({ PatientID });
2019-12-11 13:14:01 -03:00
if (isStudyLoaded) {
this.measurementApi.retrieveMeasurements(PatientID, [
2019-12-11 13:14:01 -03:00
currentTimepointId,
]);
}
const activeViewport = this.props.viewports[this.props.activeViewportIndex];
const activeDisplaySetInstanceUID =
activeViewport ? activeViewport.displaySetInstanceUID : undefined;
2019-07-04 14:09:13 -04:00
this.setState({
thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID),
2019-07-04 14:09:13 -04:00
});
}
}
componentDidUpdate(prevProps) {
const {
studies,
isStudyLoaded,
activeViewportIndex,
viewports
} = this.props;
const activeViewport = viewports[activeViewportIndex];
const activeDisplaySetInstanceUID =
activeViewport ? activeViewport.displaySetInstanceUID : undefined;
const prevActiveViewport = prevProps.viewports[prevProps.activeViewportIndex];
const prevActiveDisplaySetInstanceUID =
prevActiveViewport ? prevActiveViewport.displaySetInstanceUID : undefined;
if (studies !== prevProps.studies ||
activeViewportIndex !== prevProps.activeViewportIndex ||
activeDisplaySetInstanceUID !== prevActiveDisplaySetInstanceUID
) {
2019-12-11 13:14:01 -03:00
this.setState({
thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID),
2019-12-11 13:14:01 -03:00
});
}
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
const PatientID = studies[0] && studies[0].PatientID;
2019-12-11 13:14:01 -03:00
const { currentTimepointId } = this;
2019-07-04 14:09:13 -04:00
this.timepointApi.retrieveTimepoints({ PatientID });
this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]);
}
}
_getActiveViewport() {
return this.props.viewports[this.props.activeViewportIndex];
}
render() {
2019-07-04 14:09:13 -04:00
let VisiblePanelLeft, VisiblePanelRight;
const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL];
panelExtensions.forEach(panelExt => {
panelExt.module.components.forEach(comp => {
if (comp.id === this.state.selectedRightSidePanel) {
VisiblePanelRight = comp.component;
} else if (comp.id === this.state.selectedLeftSidePanel) {
VisiblePanelLeft = comp.component;
}
});
});
return (
<>
2019-07-04 14:09:13 -04:00
{/* HEADER */}
<WhiteLabelingContext.Consumer>
{whiteLabeling => (
<UserManagerContext.Consumer>
2019-08-13 15:36:46 -04:00
{userManager => (
2020-04-17 12:26:09 -04:00
<AppContext.Consumer>
{appContext => (
<ConnectedHeader
linkText={
appContext.appConfig.showStudyList
? 'Study List'
: undefined
}
linkPath={
appContext.appConfig.showStudyList ? '/' : undefined
}
userManager={userManager}
>
{whiteLabeling &&
whiteLabeling.createLogoComponentFn &&
whiteLabeling.createLogoComponentFn(React)}
2020-04-17 12:26:09 -04:00
</ConnectedHeader>
)}
</AppContext.Consumer>
2019-08-13 15:36:46 -04:00
)}
</UserManagerContext.Consumer>
)}
</WhiteLabelingContext.Consumer>
2019-07-04 14:09:13 -04:00
{/* TOOLBAR */}
2020-06-04 06:52:30 -03:00
<ErrorBoundaryDialog context="ToolbarRow">
<ToolbarRow
activeViewport={
this.props.viewports[this.props.activeViewportIndex]
}
isDerivedDisplaySetsLoaded={this.props.isDerivedDisplaySetsLoaded}
2020-06-04 06:52:30 -03:00
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
selectedLeftSidePanel={
this.state.isLeftSidePanelOpen
? this.state.selectedLeftSidePanel
: ''
2019-07-04 14:09:13 -04:00
}
2020-06-04 06:52:30 -03:00
selectedRightSidePanel={
this.state.isRightSidePanelOpen
? this.state.selectedRightSidePanel
: ''
}
handleSidePanelChange={(side, selectedPanel) => {
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
const openKey = `is${sideClicked}SidePanelOpen`;
const selectedKey = `selected${sideClicked}SidePanel`;
const updatedState = Object.assign({}, this.state);
const isOpen = updatedState[openKey];
const prevSelectedPanel = updatedState[selectedKey];
// RoundedButtonGroup returns `null` if selected button is clicked
const isSameSelectedPanel =
prevSelectedPanel === selectedPanel || selectedPanel === null;
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
if (isClosedOrShouldClose) {
updatedState[openKey] = !updatedState[openKey];
}
this.setState(updatedState);
}}
studies={this.props.studies}
/>
</ErrorBoundaryDialog>
2019-07-04 14:09:13 -04:00
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
{/*<StudyPrefetcher studies={this.props.studies} />*/}
{/* VIEWPORTS + SIDEPANELS */}
<div className="FlexboxLayout">
{/* LEFT */}
2020-06-04 06:52:30 -03:00
<ErrorBoundaryDialog context="LeftSidePanel">
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
{VisiblePanelLeft ? (
<VisiblePanelLeft
viewports={this.props.viewports}
studies={this.props.studies}
activeIndex={this.props.activeViewportIndex}
/>
) : (
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
2020-06-04 06:52:30 -03:00
</SidePanel>
</ErrorBoundaryDialog>
2019-07-04 14:09:13 -04:00
{/* MAIN */}
<div className={classNames('main-content')}>
2020-06-04 06:52:30 -03:00
<ErrorBoundaryDialog context="ViewerMain">
<ConnectedViewerMain
studies={this.props.studies}
isStudyLoaded={this.props.isStudyLoaded}
/>
</ErrorBoundaryDialog>
2019-07-04 14:09:13 -04:00
</div>
{/* RIGHT */}
2020-06-04 06:52:30 -03:00
<ErrorBoundaryDialog context="RightSidePanel">
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
{VisiblePanelRight && (
<VisiblePanelRight
isOpen={this.state.isRightSidePanelOpen}
viewports={this.props.viewports}
studies={this.props.studies}
activeIndex={this.props.activeViewportIndex}
activeViewport={
this.props.viewports[this.props.activeViewportIndex]
}
getActiveViewport={this._getActiveViewport}
2020-06-04 06:52:30 -03:00
/>
)}
</SidePanel>
</ErrorBoundaryDialog>
</div>
</>
);
}
2018-12-14 15:23:48 +01:00
}
export default withDialog(Viewer);
2019-07-04 14:09:13 -04:00
/**
* Async function to check if there are any inconsistences in the series.
*
* For segmentation checks that the geometry is consistent with the source images:
* 1) no frames out of plane;
* 2) have the same width and height.
*
* For reconstructable 3D volume:
* 1) Is series multiframe?
* 2) Do the frames have different dimensions/number of components/orientations?
* 3) Has the series any missing frames or irregular spacing?
* 4) Is the series 4D?
*
* If not reconstructable, MPR is disabled.
* The actual computations are done in isDisplaySetReconstructable.
*
* @param {*object} displaySet
* @returns {[string]} an array of strings containing the warnings
*/
const _checkForSeriesInconsistencesWarnings = async function (displaySet, studies) {
if (displaySet.inconsistencyWarnings) {
// warnings already checked and cached in displaySet
return displaySet.inconsistencyWarnings;
}
const inconsistencyWarnings = [];
if (displaySet.Modality !== 'SEG') {
if (displaySet.reconstructionIssues && displaySet.reconstructionIssues.length !== 0) {
displaySet.reconstructionIssues.forEach(warning => {
switch (warning) {
case ReconstructionIssues.DATASET_4D:
inconsistencyWarnings.push('The dataset is 4D.');
break;
case ReconstructionIssues.VARYING_IMAGESDIMENSIONS:
inconsistencyWarnings.push('The dataset frames have different dimensions (rows, columns).');
break;
case ReconstructionIssues.VARYING_IMAGESCOMPONENTS:
inconsistencyWarnings.push('The dataset frames have different components (Sample per pixel).');
break;
case ReconstructionIssues.VARYING_IMAGESORIENTATION:
inconsistencyWarnings.push('The dataset frames have different orientation.');
break;
case ReconstructionIssues.IRREGULAR_SPACING:
inconsistencyWarnings.push('The dataset frames have different pixel spacing.');
break;
case ReconstructionIssues.MULTIFFRAMES:
inconsistencyWarnings.push('The dataset is a multiframes.');
break;
default:
break;
}
});
inconsistencyWarnings.push('The datasets is not a reconstructable 3D volume. MPR mode is not available.');
}
if (displaySet.missingFrames &&
(!displaySet.reconstructionIssues ||
(displaySet.reconstructionIssues && !displaySet.reconstructionIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) {
inconsistencyWarnings.push('The datasets is missing frames: ' + displaySet.missingFrames + '.');
}
} else {
const segMetadata = displaySet.metadata;
if (!segMetadata) {
displaySet.inconsistencyWarnings = inconsistencyWarnings;
return inconsistencyWarnings;
}
2021-04-20 16:28:38 +02:00
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false);
if (!referencedDisplaySet) {
displaySet.inconsistencyWarnings = inconsistencyWarnings;
return inconsistencyWarnings;
2021-04-19 14:53:38 +02:00
}
2021-04-20 16:28:38 +02:00
const imageIds = referencedDisplaySet.images.map(image => image.getImageId());
if (!imageIds || imageIds.length === 0) {
displaySet.inconsistencyWarnings = inconsistencyWarnings;
return inconsistencyWarnings;
}
for (
let i = 0, groupsLen = segMetadata.PerFrameFunctionalGroupsSequence.length;
i < groupsLen;
++i
) {
const PerFrameFunctionalGroups = segMetadata.PerFrameFunctionalGroupsSequence[i];
if (!PerFrameFunctionalGroups) {
continue;
}
let SourceImageSequence = undefined;
if (segMetadata.SourceImageSequence) {
SourceImageSequence = segMetadata.SourceImageSequence[i];
} else if (PerFrameFunctionalGroups.DerivationImageSequence) {
SourceImageSequence =
PerFrameFunctionalGroups.DerivationImageSequence
.SourceImageSequence;
}
if (!SourceImageSequence) {
if (inconsistencyWarnings.length === 0) {
const warningMessage = 'The segmentation ' +
'has frames out of plane respect to the source images.';
inconsistencyWarnings.push(warningMessage);
}
continue;
}
const {
ReferencedSOPInstanceUID,
} = SourceImageSequence;
const imageId = imageIds.find(imageId => {
const sopCommonModule = cornerstone.metaData.get(
"sopCommonModule",
imageId
);
if (!sopCommonModule) {
return;
}
return sopCommonModule.sopInstanceUID === ReferencedSOPInstanceUID;
});
if (!imageId) {
continue;
}
const sourceImageMetadata = cornerstone.metaData.get(
"instance",
imageId
);
if (
segMetadata.Rows !== sourceImageMetadata.Rows ||
segMetadata.Columns !== sourceImageMetadata.Columns
) {
const warningMessage = 'The segmentation ' +
'has frames with different geometry ' +
'dimensions (Rows and Columns) respect to the source images.';
inconsistencyWarnings.push(warningMessage);
break;
}
}
if (inconsistencyWarnings.length !== 0) {
const warningMessage = 'The segmentation format is not supported yet. ' +
'The segmentation data (segments) could not be loaded.';
inconsistencyWarnings.push(warningMessage);
}
}
// cache the warnings
displaySet.inconsistencyWarnings = inconsistencyWarnings;
return inconsistencyWarnings;
}
/**
* Checks if display set is active, i.e. if the series is currently shown
* in the active viewport.
*
* For data display set, this functions checks if the active
* display set instance uid in the current active viewport is the same of the
* thumbnail one.
*
* For derived modalities (e.g., SEG and RTSTRUCT), the function gets the
* reference display set and then checks the reference uid with the active
* display set instance uid.
*
* @param {displaySet} displaySet
* @param {Study[]} studies
* @param {string} activeDisplaySetInstanceUID
* @returns {boolean} is active.
*/
const _isDisplaySetActive = function(displaySet, studies, activeDisplaySetInstanceUID) {
let active = false;
const {
displaySetInstanceUID,
} = displaySet;
// TO DO: in the future, we could possibly support new modalities
// we should have a list of all modalities here, instead of having hard coded checks
if (displaySet.Modality !== 'SEG' &&
displaySet.Modality !== 'RTSTRUCT' &&
displaySet.Modality !== 'RTDOSE') {
active = activeDisplaySetInstanceUID === displaySetInstanceUID;
} else if (displaySet.getSourceDisplaySet){
if (displaySet.Modality === 'SEG') {
const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false);
active = referencedDisplaySet ?
activeDisplaySetInstanceUID === referencedDisplaySet.displaySetInstanceUID :
false;
} else {
const referencedDisplaySet = displaySet.getSourceDisplaySet(studies, false);
active = referencedDisplaySet ?
activeDisplaySetInstanceUID === referencedDisplaySet.displaySetInstanceUID :
false;
}
}
return active;
};
2019-07-04 14:09:13 -04:00
/**
* What types are these? Why do we have "mapping" dropped in here instead of in
* a mapping layer?
*
* TODO[react]:
* - Add showStackLoadingProgressBar option
*
* @param {Study[]} studies
* @param {string} activeDisplaySetInstanceUID
2019-07-04 14:09:13 -04:00
*/
const _mapStudiesToThumbnails = function(studies, activeDisplaySetInstanceUID) {
2019-07-04 14:09:13 -04:00
return studies.map(study => {
const { StudyInstanceUID } = study;
2019-07-04 14:09:13 -04:00
const thumbnails = study.displaySets.map(displaySet => {
const {
displaySetInstanceUID,
SeriesDescription,
InstanceNumber,
2019-07-04 14:09:13 -04:00
numImageFrames,
SeriesNumber,
2019-07-04 14:09:13 -04:00
} = displaySet;
let imageId;
let altImageText;
if (displaySet.Modality && displaySet.Modality === 'SEG') {
// TODO: We want to replace this with a thumbnail showing
// the segmentation map on the image, but this is easier
// and better than what we have right now.
altImageText = 'SEG';
} else if (displaySet.images && displaySet.images.length) {
const imageIndex = Math.floor(displaySet.images.length / 2);
imageId = displaySet.images[imageIndex].getImageId();
2019-07-04 14:09:13 -04:00
} else {
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
2019-07-04 14:09:13 -04:00
}
const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet, studies);
const active = _isDisplaySetActive(displaySet, studies, activeDisplaySetInstanceUID)
2019-07-04 14:09:13 -04:00
return {
active,
2019-07-04 14:09:13 -04:00
imageId,
altImageText,
displaySetInstanceUID,
SeriesDescription,
InstanceNumber,
2019-07-04 14:09:13 -04:00
numImageFrames,
SeriesNumber,
hasWarnings,
2019-07-04 14:09:13 -04:00
};
});
return {
StudyInstanceUID,
2019-07-04 14:09:13 -04:00
thumbnails,
};
});
};