2019-01-24 14:00:01 +01:00
|
|
|
import React, { Component } from 'react';
|
|
|
|
|
import PropTypes from 'prop-types';
|
2019-07-04 14:09:13 -04:00
|
|
|
import classNames from 'classnames';
|
2018-12-29 12:02:18 -02:00
|
|
|
|
2019-07-04 14:09:13 -04:00
|
|
|
import { MODULE_TYPES } from 'ohif-core';
|
2019-01-31 22:01:39 -05:00
|
|
|
import OHIF from 'ohif-core';
|
|
|
|
|
import moment from 'moment';
|
2019-01-24 14:00:01 +01:00
|
|
|
import WhiteLabellingContext from '../WhiteLabellingContext.js';
|
2019-01-31 05:55:05 -02:00
|
|
|
import ConnectedHeader from './ConnectedHeader.js';
|
|
|
|
|
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
2019-05-02 17:12:06 -04:00
|
|
|
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
|
2019-07-04 14:09:13 -04:00
|
|
|
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
|
|
|
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
|
|
|
|
import SidePanel from './../components/SidePanel.js';
|
|
|
|
|
import { extensionManager } from './../App.js';
|
2019-07-21 16:43:42 +02:00
|
|
|
import UserManagerContext from '../UserManagerContext';
|
2018-12-18 13:33:26 +01:00
|
|
|
import './Viewer.css';
|
2018-12-14 15:23:48 +01:00
|
|
|
/**
|
|
|
|
|
* Inits OHIF Hanging Protocol's onReady.
|
|
|
|
|
* It waits for OHIF Hanging Protocol to be ready to instantiate the ProtocolEngine
|
|
|
|
|
* Hanging Protocol will use OHIF LayoutManager to render viewports properly
|
|
|
|
|
*/
|
|
|
|
|
/*const initHangingProtocol = () => {
|
|
|
|
|
// When Hanging Protocol is ready
|
|
|
|
|
HP.ProtocolStore.onReady(() => {
|
|
|
|
|
|
|
|
|
|
// Gets all StudyMetadata objects: necessary for Hanging Protocol to access study metadata
|
|
|
|
|
const studyMetadataList = OHIF.viewer.StudyMetadataList.all();
|
|
|
|
|
|
|
|
|
|
// Instantiate StudyMetadataSource: necessary for Hanging Protocol to get study metadata
|
|
|
|
|
const studyMetadataSource = new OHIF.studies.classes.OHIFStudyMetadataSource();
|
|
|
|
|
|
|
|
|
|
// Get prior studies map
|
|
|
|
|
const studyPriorsMap = OHIF.studylist.functions.getStudyPriorsMap(studyMetadataList);
|
|
|
|
|
|
|
|
|
|
// Creates Protocol Engine object with required arguments
|
|
|
|
|
const ProtocolEngine = new HP.ProtocolEngine(layoutManager, studyMetadataList, studyPriorsMap, studyMetadataSource);
|
|
|
|
|
|
|
|
|
|
// Sets up Hanging Protocol engine
|
|
|
|
|
HP.setEngine(ProtocolEngine);
|
|
|
|
|
});
|
|
|
|
|
};*/
|
|
|
|
|
|
2018-12-17 17:23:03 +01:00
|
|
|
/*const viewportUtils = OHIF.viewerbase.viewportUtils;
|
2018-12-14 15:23:48 +01:00
|
|
|
|
|
|
|
|
OHIF.viewer.functionList = {
|
|
|
|
|
toggleCineDialog: viewportUtils.toggleCineDialog,
|
|
|
|
|
toggleCinePlay: viewportUtils.toggleCinePlay,
|
|
|
|
|
clearTools: viewportUtils.clearTools,
|
|
|
|
|
resetViewport: viewportUtils.resetViewport,
|
|
|
|
|
invert: viewportUtils.invert
|
2018-12-17 17:23:03 +01:00
|
|
|
};*/
|
2018-12-14 15:23:48 +01:00
|
|
|
|
|
|
|
|
class Viewer extends Component {
|
2019-01-24 14:00:01 +01:00
|
|
|
static propTypes = {
|
2019-02-04 20:46:15 -05:00
|
|
|
studies: PropTypes.array,
|
2019-02-08 17:03:11 -02:00
|
|
|
studyInstanceUids: PropTypes.array,
|
2019-02-04 20:46:15 -05:00
|
|
|
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-01-24 14:00:01 +01:00
|
|
|
};
|
|
|
|
|
|
2019-01-31 22:01:39 -05:00
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
OHIF.measurements.MeasurementApi.setConfiguration({
|
|
|
|
|
dataExchange: {
|
2019-02-04 20:46:15 -05:00
|
|
|
retrieve: this.retrieveMeasurements,
|
2019-05-29 08:39:54 -04:00
|
|
|
store: this.storeMeasurements,
|
|
|
|
|
},
|
2019-01-31 22:01:39 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
OHIF.measurements.TimepointApi.setConfiguration({
|
|
|
|
|
dataExchange: {
|
2019-02-04 20:46:15 -05:00
|
|
|
retrieve: this.retrieveTimepoints,
|
|
|
|
|
store: this.storeTimepoints,
|
|
|
|
|
remove: this.removeTimepoint,
|
|
|
|
|
update: this.updateTimepoint,
|
2019-05-29 08:39:54 -04:00
|
|
|
disassociate: this.disassociateStudy,
|
|
|
|
|
},
|
2019-01-31 22:01:39 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-04 14:09:13 -04:00
|
|
|
state = {
|
|
|
|
|
isLeftSidePanelOpen: true,
|
|
|
|
|
isRightSidePanelOpen: false,
|
|
|
|
|
selectedRightSidePanel: '',
|
|
|
|
|
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
|
|
|
|
|
thumbnails: [],
|
|
|
|
|
};
|
|
|
|
|
|
2019-02-04 20:46:15 -05:00
|
|
|
retrieveMeasurements = (patientId, timepointIds) => {
|
|
|
|
|
OHIF.log.info('retrieveMeasurements');
|
|
|
|
|
// TODO: Retrieve the measurements from the latest available SR
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
storeMeasurements = (measurementData, timepointIds) => {
|
|
|
|
|
OHIF.log.info('storeMeasurements');
|
|
|
|
|
// TODO: Store the measurements into a new SR sent to the active server
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
},
|
2019-02-04 20:46:15 -05:00
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-01-31 22:01:39 -05:00
|
|
|
componentDidMount() {
|
|
|
|
|
const { studies } = this.props;
|
|
|
|
|
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
2019-02-08 17:03:11 -02:00
|
|
|
const currentTimepointId = 'TimepointId';
|
2019-01-31 22:01:39 -05:00
|
|
|
|
2019-02-08 17:03:11 -02:00
|
|
|
const timepointApi = new TimepointApi(currentTimepointId, {
|
2019-05-29 08:39:54 -04:00
|
|
|
onTimepointsUpdated: this.onTimepointsUpdated,
|
2019-02-04 20:46:15 -05:00
|
|
|
});
|
2019-02-08 17:03:11 -02:00
|
|
|
|
2019-02-04 20:46:15 -05:00
|
|
|
const measurementApi = new MeasurementApi(timepointApi, {
|
2019-05-29 08:39:54 -04:00
|
|
|
onMeasurementsUpdated: this.onMeasurementsUpdated,
|
2019-02-04 20:46:15 -05:00
|
|
|
});
|
2019-01-31 22:01:39 -05:00
|
|
|
|
2019-06-06 11:11:26 -03:00
|
|
|
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
|
|
|
|
2019-06-06 11:11:26 -03:00
|
|
|
timepointApi.retrieveTimepoints({ patientId });
|
|
|
|
|
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
2019-07-04 14:09:13 -04:00
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
thumbnails: _mapStudiesToThumbnails(studies),
|
|
|
|
|
});
|
2019-06-06 11:11:26 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
|
|
|
|
if (this.props.studies !== prevProps.studies) {
|
|
|
|
|
const { studies } = this.props;
|
|
|
|
|
const patientId = studies[0] && studies[0].patientId;
|
|
|
|
|
const currentTimepointId = this.currentTimepointId;
|
2019-07-04 14:09:13 -04:00
|
|
|
|
2019-06-06 11:11:26 -03:00
|
|
|
this.timepointApi.retrieveTimepoints({ patientId });
|
|
|
|
|
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
2019-07-04 14:09:13 -04:00
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
thumbnails: _mapStudiesToThumbnails(studies),
|
|
|
|
|
});
|
2019-06-06 11:11:26 -03:00
|
|
|
}
|
2019-01-31 22:01:39 -05:00
|
|
|
}
|
|
|
|
|
|
2019-01-24 14:00:01 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2019-01-24 14:00:01 +01:00
|
|
|
return (
|
|
|
|
|
<>
|
2019-07-04 14:09:13 -04:00
|
|
|
{/* HEADER */}
|
2019-01-24 14:00:01 +01:00
|
|
|
<WhiteLabellingContext.Consumer>
|
|
|
|
|
{whiteLabelling => (
|
2019-07-21 16:43:42 +02:00
|
|
|
<UserManagerContext.Consumer>
|
|
|
|
|
{ userManager => (
|
|
|
|
|
<ConnectedHeader home={false} userManager={userManager}>
|
|
|
|
|
{whiteLabelling.logoComponent}
|
|
|
|
|
</ConnectedHeader>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
</UserManagerContext.Consumer>
|
2019-01-24 14:00:01 +01:00
|
|
|
)}
|
|
|
|
|
</WhiteLabellingContext.Consumer>
|
2019-07-04 14:09:13 -04:00
|
|
|
|
|
|
|
|
{/* TOOLBAR */}
|
|
|
|
|
<ConnectedToolbarRow
|
|
|
|
|
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
|
|
|
|
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
|
|
|
|
selectedLeftSidePanel={
|
|
|
|
|
this.state.isLeftSidePanelOpen
|
|
|
|
|
? this.state.selectedLeftSidePanel
|
|
|
|
|
: ''
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
|
|
|
|
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
|
|
|
|
|
|
|
|
|
{/* VIEWPORTS + SIDEPANELS */}
|
|
|
|
|
<div className="FlexboxLayout">
|
|
|
|
|
{/* LEFT */}
|
|
|
|
|
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
|
|
|
|
{VisiblePanelLeft ? (
|
|
|
|
|
<VisiblePanelLeft
|
|
|
|
|
viewports={this.props.viewports}
|
|
|
|
|
activeIndex={this.props.activeViewportIndex}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<ConnectedStudyBrowser studies={this.state.thumbnails} />
|
|
|
|
|
)}
|
|
|
|
|
</SidePanel>
|
|
|
|
|
|
|
|
|
|
{/* MAIN */}
|
|
|
|
|
<div className={classNames('main-content')}>
|
|
|
|
|
<ConnectedViewerMain studies={this.props.studies} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* RIGHT */}
|
|
|
|
|
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
|
|
|
|
{VisiblePanelRight && (
|
|
|
|
|
<VisiblePanelRight
|
|
|
|
|
viewports={this.props.viewports}
|
|
|
|
|
activeIndex={this.props.activeViewportIndex}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</SidePanel>
|
2019-01-24 14:00:01 +01:00
|
|
|
</div>
|
2019-07-04 14:09:13 -04:00
|
|
|
<ConnectedLabellingOverlay />
|
2019-01-24 14:00:01 +01:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-12-14 15:23:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default Viewer;
|
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 sorting of display sets
|
|
|
|
|
* - Add showStackLoadingProgressBar option
|
|
|
|
|
*
|
|
|
|
|
* @param {Study[]} studies
|
|
|
|
|
* @param {DisplaySet[]} studies[].displaySets
|
|
|
|
|
*/
|
|
|
|
|
const _mapStudiesToThumbnails = function(studies) {
|
|
|
|
|
return studies.map(study => {
|
|
|
|
|
const { studyInstanceUid } = study;
|
|
|
|
|
|
|
|
|
|
const thumbnails = study.displaySets.map(displaySet => {
|
|
|
|
|
const {
|
|
|
|
|
displaySetInstanceUid,
|
|
|
|
|
seriesDescription,
|
|
|
|
|
seriesNumber,
|
|
|
|
|
instanceNumber,
|
|
|
|
|
numImageFrames,
|
|
|
|
|
} = displaySet;
|
|
|
|
|
|
|
|
|
|
let imageId;
|
2019-07-22 13:29:12 +02:00
|
|
|
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) {
|
2019-07-19 18:29:34 +02:00
|
|
|
const imageIndex = Math.floor(displaySet.images.length / 2);
|
|
|
|
|
|
|
|
|
|
imageId = displaySet.images[imageIndex].getImageId();
|
2019-07-04 14:09:13 -04:00
|
|
|
} else {
|
2019-07-15 11:09:58 -03:00
|
|
|
altImageText = displaySet.modality ? displaySet.modality : 'UN';
|
2019-07-04 14:09:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
imageId,
|
|
|
|
|
altImageText,
|
|
|
|
|
displaySetInstanceUid,
|
|
|
|
|
seriesDescription,
|
|
|
|
|
seriesNumber,
|
|
|
|
|
instanceNumber,
|
|
|
|
|
numImageFrames,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
studyInstanceUid,
|
|
|
|
|
thumbnails,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|