import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useLogger } from '@ohif/ui'; 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'; import { ReconstructionIssues } from './../../../core/src/enums.js'; import dcmjs from 'dcmjs'; // Contexts import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; import UserManagerContext from '../context/UserManagerContext'; import AppContext from '../context/AppContext'; import './Viewer.css'; import { finished } from 'stream'; import { cornerstoneWADOImageLoader } from 'cornerstone-wado-image-loader'; 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, }, }); this._getActiveViewport = this._getActiveViewport.bind(this); } 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]); } } _getActiveViewport() { return this.props.viewports[this.props.activeViewportIndex]; } 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); /** * 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) { const warningsList = []; if (displaySet.Modality !== 'SEG') { if (displaySet.warningIssues && displaySet.warningIssues.length !== 0) { displaySet.warningIssues.forEach(warning => { switch (warning) { case ReconstructionIssues.DATASET_4D: warningsList.push('The dataset is 4D.'); break; case ReconstructionIssues.VARYING_IMAGESDIMENSIONS: warningsList.push('The dataset frames have different dimensions (rows, columns).'); break; case ReconstructionIssues.VARYING_IMAGESCOMPONENTS: warningsList.push('The dataset frames have different components (Sample per pixel).'); break; case ReconstructionIssues.VARYING_IMAGESORIENTATION: warningsList.push('The dataset frames have different orientation.'); break; case ReconstructionIssues.IRREGULAR_SPACING: warningsList.push('The dataset frames have different pixel spacing.'); break; case ReconstructionIssues.MULTIFFRAMES: warningsList.push('The dataset is a multiframes.'); break; default: break; } }); warningsList.push('The datasets is not a reconstructable 3D volume. MPR mode is not available.'); } if (displaySet.missingFrames && (!displaySet.warningIssues || (displaySet.warningIssues && !displaySet.warningIssues.find(warn => warn === ReconstructionIssues.DATASET_4D)))) { warningsList.push('The datasets is missing frames: ' + displaySet.missingFrames + '.'); } } else { const segMetadata = displaySet.metadata; if (!segMetadata) { return warningsList; } const sourceDisplaySet = displaySet.getSourceDisplaySet(studies, false); const imageIds = sourceDisplaySet.images.map(image => image.getImageId()); if (!imageIds || imageIds.length === 0) { return warningsList; } 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 (warningsList.length === 0) { const warningMessage = 'The segmentation ' + 'has frames out of plane respect to the source images.'; warningsList.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.'; warningsList.push(warningMessage); break; } } if (warningsList.length !== 0) { const warningMessage = 'The segmentation format is not supported yet. ' + 'The segmentation data (segments) could not be loaded.'; warningsList.push(warningMessage); } } return warningsList; } /** * 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 {DisplaySet[]} studies[].displaySets */ const _mapStudiesToThumbnails = function(studies) { return studies.map(study => { const { StudyInstanceUID } = study; const thumbnails = study.displaySets.map(displaySet => { const { displaySetInstanceUID, SeriesDescription, InstanceNumber, numImageFrames, SeriesNumber, } = 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'; } const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet, studies); return { imageId, altImageText, displaySetInstanceUID, SeriesDescription, InstanceNumber, numImageFrames, SeriesNumber, hasWarnings, }; }); return { StudyInstanceUID, thumbnails, }; }); };