import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core'; import { withDialog } from '@ohif/ui'; import moment from 'moment'; import ConnectedHeader from './ConnectedHeader.js'; import ToolbarRow from './ToolbarRow.js'; import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; import ConnectedViewerMain from './ConnectedViewerMain.js'; import SidePanel from './../components/SidePanel.js'; import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog'; import { extensionManager } from './../App.js'; // Contexts import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; import UserManagerContext from '../context/UserManagerContext'; import AppContext from '../context/AppContext'; import './Viewer.css'; class Viewer extends Component { static propTypes = { studies: PropTypes.arrayOf( PropTypes.shape({ StudyInstanceUID: PropTypes.string.isRequired, StudyDate: PropTypes.string, PatientID: PropTypes.string, displaySets: PropTypes.arrayOf( PropTypes.shape({ displaySetInstanceUID: PropTypes.string.isRequired, SeriesDescription: PropTypes.string, SeriesNumber: PropTypes.number, InstanceNumber: PropTypes.number, numImageFrames: PropTypes.number, Modality: PropTypes.string.isRequired, images: PropTypes.arrayOf( PropTypes.shape({ getImageId: PropTypes.func.isRequired, }) ), }) ), }) ), studyInstanceUIDs: PropTypes.array, activeServer: PropTypes.shape({ type: PropTypes.string, wadoRoot: PropTypes.string, }), onTimepointsUpdated: PropTypes.func, onMeasurementsUpdated: PropTypes.func, // window.store.getState().viewports.viewportSpecificData viewports: PropTypes.object.isRequired, // window.store.getState().viewports.activeViewportIndex activeViewportIndex: PropTypes.number.isRequired, isStudyLoaded: PropTypes.bool, dialog: PropTypes.object, }; constructor(props) { super(props); const { activeServer } = this.props; const server = Object.assign({}, activeServer); OHIF.measurements.MeasurementApi.setConfiguration({ dataExchange: { retrieve: DICOMSR.retrieveMeasurements, store: DICOMSR.storeMeasurements, }, server, }); OHIF.measurements.TimepointApi.setConfiguration({ dataExchange: { retrieve: this.retrieveTimepoints, store: this.storeTimepoints, remove: this.removeTimepoint, update: this.updateTimepoint, disassociate: this.disassociateStudy, }, }); } 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, 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() { const { studies, isStudyLoaded } = this.props; const { TimepointApi, MeasurementApi } = OHIF.measurements; const currentTimepointId = 'TimepointId'; const timepointApi = new TimepointApi(currentTimepointId, { onTimepointsUpdated: this.onTimepointsUpdated, }); const measurementApi = new MeasurementApi(timepointApi, { onMeasurementsUpdated: this.onMeasurementsUpdated, }); this.currentTimepointId = currentTimepointId; this.timepointApi = timepointApi; this.measurementApi = measurementApi; if (studies) { const PatientID = studies[0] && studies[0].PatientID; timepointApi.retrieveTimepoints({ PatientID }); if (isStudyLoaded) { this.measurementApi.retrieveMeasurements(PatientID, [ currentTimepointId, ]); } this.setState({ thumbnails: _mapStudiesToThumbnails(studies), }); } } componentDidUpdate(prevProps) { const { studies, isStudyLoaded } = this.props; if (studies !== prevProps.studies) { this.setState({ thumbnails: _mapStudiesToThumbnails(studies), }); } if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) { const PatientID = studies[0] && studies[0].PatientID; const { currentTimepointId } = this; this.timepointApi.retrieveTimepoints({ PatientID }); this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]); } } render() { 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 ( <> {/* HEADER */} {whiteLabeling => ( {userManager => ( {appContext => ( {whiteLabeling && whiteLabeling.createLogoComponentFn && whiteLabeling.createLogoComponentFn(React)} )} )} )} {/* TOOLBAR */} { 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} /> {/**/} {/**/} {/* VIEWPORTS + SIDEPANELS */}
{/* LEFT */} {VisiblePanelLeft ? ( ) : ( )} {/* MAIN */}
{/* RIGHT */} {VisiblePanelRight && ( )}
); } } export default withDialog(Viewer); /** * 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; 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(); } else { altImageText = displaySet.Modality ? displaySet.Modality : 'UN'; } return { imageId, altImageText, displaySetInstanceUID, SeriesDescription, SeriesNumber, InstanceNumber, numImageFrames, }; }); return { StudyInstanceUID, thumbnails, }; }); };