From 78f1a7f3e5df07fc6adeb10dda4f8672a4cc1d79 Mon Sep 17 00:00:00 2001 From: Davide Punzo Date: Mon, 17 May 2021 18:48:32 +0200 Subject: [PATCH] Re #2259: UI refinements for warnings (#2397) * Re #2259: highlight series thumbnails border for active series (series in the active viewport) * Re #2259: add the warning icon also on the active viewport as an overlay --- .../src/OHIFCornerstoneViewport.js | 7 + .../OHIFCornerstoneViewportOverlay.css | 69 ++++++++ .../OHIFCornerstoneViewportOverlay.js | 167 ++++++++++++++++++ .../cornerstone/src/utils/formatStudy.js | 109 ++++++++++++ .../src/OHIFDicomRTStructSopClassHandler.js | 4 +- .../dicom-rt/src/getSourceDisplaySet.js | 38 +--- .../src/getSourceDisplaySet.js | 4 +- .../src/classes/metadata/StudyMetadata.js | 131 +++++++------- .../src/utils/isDisplaySetReconstructable.js | 32 ++-- .../utils/loadAndCacheDerivedDisplaySets.js | 7 +- .../components/studyBrowser/ImageThumbnail.js | 6 +- .../studyBrowser/ImageThumbnail.styl | 4 +- .../components/studyBrowser/StudyBrowser.js | 2 + .../src/components/studyBrowser/Thumbnail.js | 25 +-- .../components/studyBrowser/Thumbnail.styl | 9 +- platform/viewer/CHANGELOG.md | 1 + .../common/OHIFCornerstoneHotkeys.spec.js | 2 +- .../common/OHIFUserPreferences.spec.js | 2 +- platform/viewer/cypress/support/aliases.js | 4 +- .../viewer/src/connectedComponents/Viewer.js | 136 ++++++++++---- 20 files changed, 598 insertions(+), 161 deletions(-) create mode 100644 extensions/cornerstone/src/components/OHIFCornerstoneViewportOverlay.css create mode 100644 extensions/cornerstone/src/components/OHIFCornerstoneViewportOverlay.js create mode 100644 extensions/cornerstone/src/utils/formatStudy.js diff --git a/extensions/cornerstone/src/OHIFCornerstoneViewport.js b/extensions/cornerstone/src/OHIFCornerstoneViewport.js index a145926c1..b2c7d9eb9 100644 --- a/extensions/cornerstone/src/OHIFCornerstoneViewport.js +++ b/extensions/cornerstone/src/OHIFCornerstoneViewport.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; +import OHIFCornerstoneViewportOverlay from './components/OHIFCornerstoneViewportOverlay' import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport'; import OHIF from '@ohif/core'; import PropTypes from 'prop-types'; @@ -194,6 +195,7 @@ class OHIFCornerstoneViewport extends Component { return null; } const { viewportIndex } = this.props; + const { inconsistencyWarnings } = this.props.viewportData.displaySet; const { imageIds, currentImageIdIndex, @@ -229,6 +231,10 @@ class OHIFCornerstoneViewport extends Component { } }; + const warningsOverlay = props => { + return + }; + return ( <> { + if (Array.isArray(warningList)) { + const listedWarnings = warningList.map((warn, index) => { + return
  • {warn}
  • ; + }); + + return
      {listedWarnings}
    ; + } else { + return {warningList}; + } + }; + + const getWarningInfo = (seriesNumber, inconsistencyWarnings) => { + return( + + {inconsistencyWarnings.length != 0 ? ( + +
    Series Inconsistencies
    +
    {getWarningContent(inconsistencyWarnings)}
    + + } + > +
    + + + +
    +
    + ) : ( + + )} +
    + ); + }; + + const normal = ( + +
    +
    {formatPN(patientName)}
    +
    {patientId}
    +
    +
    +
    {studyDescription}
    +
    + {formatDICOMDate(studyDate)} {formatDICOMTime(studyTime)} +
    +
    +
    +
    Zoom: {zoomPercentage}%
    +
    {wwwc}
    +
    {compression}
    +
    +
    +
    {inconsistencyWarningsOn ? getWarningInfo(seriesNumber, inconsistencyWarnings) : ''}
    +
    +
    +
    {seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}
    +
    + {stackSize > 1 + ? `Img: ${instanceNumber} ${imageIndex}/${stackSize}` + : ''} +
    +
    + {frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''} +
    {imageDimensions}
    +
    + {isValidNumber(sliceLocation) + ? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm ` + : ''} + {sliceThickness + ? `Thick: ${formatNumberPrecision(sliceThickness, 2)} mm` + : ''} +
    +
    {seriesDescription}
    +
    +
    +
    + ); + + return
    {normal}
    ; + } +} + +export default OHIFCornerstoneViewportOverlay; diff --git a/extensions/cornerstone/src/utils/formatStudy.js b/extensions/cornerstone/src/utils/formatStudy.js new file mode 100644 index 000000000..f4559e7f0 --- /dev/null +++ b/extensions/cornerstone/src/utils/formatStudy.js @@ -0,0 +1,109 @@ +import moment from 'moment'; + +/** + * Checks if value is valid. + * + * @param {number} value + * @returns {boolean} is valid. + */ +function isValidNumber(value) { + return typeof value === 'number' && !isNaN(value); +} + +/** + * Formats number precision. + * + * @param {number} number + * @param {number} precision + * @returns {number} formatted number. + */ +function formatNumberPrecision(number, precision) { + if (number !== null) { + return parseFloat(number).toFixed(precision); + } +} + +/** + * Formats DICOM date. + * + * @param {string} date + * @param {string} strFormat + * @returns {string} formatted date. + */ +function formatDICOMDate(date, strFormat = 'MMM D, YYYY') { + return moment(date, 'YYYYMMDD').format(strFormat); +} + +/** + * DICOM Time is stored as HHmmss.SSS, where: + * HH 24 hour time: + * m mm 0..59 Minutes + * s ss 0..59 Seconds + * S SS SSS 0..999 Fractional seconds + * + * Goal: '24:12:12' + * + * @param {*} time + * @param {string} strFormat + * @returns {string} formatted name. + */ +function formatDICOMTime(time, strFormat = 'HH:mm:ss') { + return moment(time, 'HH:mm:ss').format(strFormat); +} + +/** + * Formats a patient name for display purposes + * + * @param {string} name + * @returns {string} formatted name. + */ +function formatPN(name) { + if (!name) { + return; + } + + // Convert the first ^ to a ', '. String.replace() only affects + // the first appearance of the character. + const commaBetweenFirstAndLast = name.replace('^', ', '); + + // Replace any remaining '^' characters with spaces + const cleaned = commaBetweenFirstAndLast.replace(/\^/g, ' '); + + // Trim any extraneous whitespace + return cleaned.trim(); +} + +/** + * Gets compression type + * + * @param {number} imageId + * @returns {string} comrpession type. + */ +function getCompression(imageId) { + const generalImageModule = + cornerstone.metaData.get('generalImageModule', imageId) || {}; + const { + lossyImageCompression, + lossyImageCompressionRatio, + lossyImageCompressionMethod, + } = generalImageModule; + + if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') { + const compressionMethod = lossyImageCompressionMethod || 'Lossy: '; + const compressionRatio = formatNumberPrecision( + lossyImageCompressionRatio, + 2 + ); + return compressionMethod + compressionRatio + ' : 1'; + } + + return 'Lossless / Uncompressed'; +} + +export { isValidNumber, + formatNumberPrecision, + formatDICOMDate, + formatDICOMTime, + formatPN, + getCompression +}; diff --git a/extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js b/extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js index 88f8402d4..3a5f836d4 100644 --- a/extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js +++ b/extensions/dicom-rt/src/OHIFDicomRTStructSopClassHandler.js @@ -71,8 +71,8 @@ const OHIFDicomRTStructSopClassHandler = { } } - rtStructDisplaySet.getSourceDisplaySet = function (studies) { - return getSourceDisplaySet(studies, rtStructDisplaySet); + rtStructDisplaySet.getSourceDisplaySet = function (studies, activateLabelMap = true) { + return getSourceDisplaySet(studies, rtStructDisplaySet, activateLabelMap); }; rtStructDisplaySet.load = function (referencedDisplaySet, studies) { diff --git a/extensions/dicom-rt/src/getSourceDisplaySet.js b/extensions/dicom-rt/src/getSourceDisplaySet.js index e31c036c1..ac5979226 100644 --- a/extensions/dicom-rt/src/getSourceDisplaySet.js +++ b/extensions/dicom-rt/src/getSourceDisplaySet.js @@ -1,38 +1,14 @@ -export default function getSourceDisplaySet(studies, rtStructDisplaySet) { - const referencedDisplaySet = _getReferencedDisplaySet( +import { metadata } from '@ohif/core'; + +export default function getSourceDisplaySet(studies, rtStructDisplaySet, activateLabelMap = true) { + const referencedDisplaySet = metadata.StudyMetadata.getReferencedDisplaySet( rtStructDisplaySet, studies ); - rtStructDisplaySet.load(referencedDisplaySet, studies); + if (activateLabelMap) { + rtStructDisplaySet.load(referencedDisplaySet, studies); + } return referencedDisplaySet; } - -const _getReferencedDisplaySet = (rtStructDisplaySet, studies) => { - let allDisplaySets = []; - - studies.forEach(study => { - allDisplaySets = allDisplaySets.concat(study.displaySets); - }); - - const otherDisplaySets = allDisplaySets.filter( - ds => ds.displaySetInstanceUID !== rtStructDisplaySet.displaySetInstanceUID - ); - - const ReferencedSeriesSequence = Array.isArray( - rtStructDisplaySet.metadata.ReferencedSeriesSequence - ) - ? rtStructDisplaySet.metadata.ReferencedSeriesSequence - : [rtStructDisplaySet.metadata.ReferencedSeriesSequence]; - - const referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map( - ReferencedSeries => ReferencedSeries.SeriesInstanceUID - ); - - const referencedDisplaySet = otherDisplaySets.find(ds => - referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID) - ); - - return referencedDisplaySet; -}; diff --git a/extensions/dicom-segmentation/src/getSourceDisplaySet.js b/extensions/dicom-segmentation/src/getSourceDisplaySet.js index afa423f0c..9745aad51 100644 --- a/extensions/dicom-segmentation/src/getSourceDisplaySet.js +++ b/extensions/dicom-segmentation/src/getSourceDisplaySet.js @@ -1,8 +1,8 @@ import setActiveLabelmap from './utils/setActiveLabelMap'; -import { getReferencedDisplaySet } from '../../../platform/core/src/classes/metadata/StudyMetadata.js'; +import { metadata } from '@ohif/core'; export default function getSourceDisplaySet(studies, segDisplaySet, activateLabelMap = true, onDisplaySetLoadFailureHandler) { - const referencedDisplaySet = getReferencedDisplaySet(segDisplaySet, studies); + const referencedDisplaySet = metadata.StudyMetadata.getReferencedDisplaySet(segDisplaySet, studies); let activatedLabelmapPromise; if (activateLabelMap) { diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js index 2e6531f9a..192832214 100644 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ b/platform/core/src/classes/metadata/StudyMetadata.js @@ -226,6 +226,56 @@ class StudyMetadata extends Metadata { displaySets.map(displaySet => this._derivedDisplaySets.push(displaySet)); } + + /** + * Returns the source display set of the derivated display set. + * @param {object} derivatedDisplaySet + * @param {array[StudyMetadata]} studies + * @return {object} source display set. + */ + static getReferencedDisplaySet(derivatedDisplaySet, studies) { + let allDisplaySets = []; + + studies.forEach(study => { + allDisplaySets = allDisplaySets.concat(study.displaySets); + }); + + const otherDisplaySets = allDisplaySets.filter( + ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID + ); + + const { metadata } = derivatedDisplaySet; + + let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence + (metadata, otherDisplaySets); + + let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs || + referencedSeriesInstanceUIDs.length === 0; + if (noReferencedSeriesAvailable) { + referencedSeriesInstanceUIDs = + _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence + (metadata); + } + + noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs || + referencedSeriesInstanceUIDs.length === 0; + if (noReferencedSeriesAvailable) { + referencedSeriesInstanceUIDs = + _findReferencedSeriesInstanceUIDsFromReferencedImageSequence + (metadata, otherDisplaySets); + } + + const referencedSeriesAvailable = referencedSeriesInstanceUIDs && + referencedSeriesInstanceUIDs.length !== 0; + if (referencedSeriesAvailable) { + const referencedDisplaySet = otherDisplaySets.find(ds => + referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID) + ); + ; + return referencedDisplaySet; + } + }; + /** * Returns a list of derived datasets in the study, filtered by the given filter. * @param {object} filter An object containing search filters @@ -252,7 +302,7 @@ class StudyMetadata extends Metadata { if (referencedSeriesInstanceUID) { filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter( displaySet => { - return getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID; + return StudyMetadata.getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID; } ); } @@ -810,7 +860,7 @@ const makeDisplaySet = (series, instances) => { imageSet.sortByImagePositionPatient(); // check if the spacing is uniform and update isReconstructable - const datasetIs4D = displayReconstructableInfo.warningIssues.find + const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find (issue => issue === ReconstructionIssues.DATASET_4D); displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D); imageSet.isReconstructable = displaySpacingInfo.isUniform; @@ -824,9 +874,9 @@ const makeDisplaySet = (series, instances) => { if (!imageSet.displayReconstructableInfo) { // It is not reconstrabale Save type of warning - imageSet.warningIssues = displaySpacingInfo ? - displayReconstructableInfo.warningIssues.concat(displaySpacingInfo.warningIssues) : - displayReconstructableInfo.warningIssues; + imageSet.reconstructionIssues = displaySpacingInfo ? + displayReconstructableInfo.reconstructionIssues.concat(displaySpacingInfo.reconstructionIssues) : + displayReconstructableInfo.reconstructionIssues; } return imageSet; @@ -904,55 +954,6 @@ function _getDisplaySetFromSopClassModule( return displaySet; } -/** - * Returns the source display set of the derivated display set. - * @param {object} derivatedDisplaySet - * @param {array[StudyMetadata]} studies - * @return {object} source display set. - */ -function getReferencedDisplaySet(derivatedDisplaySet, studies) { - let allDisplaySets = []; - - studies.forEach(study => { - allDisplaySets = allDisplaySets.concat(study.displaySets); - }); - - const otherDisplaySets = allDisplaySets.filter( - ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID - ); - - const { metadata } = derivatedDisplaySet; - - let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence - (metadata, otherDisplaySets); - - let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs || - referencedSeriesInstanceUIDs.length === 0; - if (noReferencedSeriesAvailable) { - referencedSeriesInstanceUIDs = - _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence - (metadata); - } - - noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs || - referencedSeriesInstanceUIDs.length === 0; - if (noReferencedSeriesAvailable) { - referencedSeriesInstanceUIDs = - _findReferencedSeriesInstanceUIDsFromReferencedImageSequence - (metadata, otherDisplaySets); - } - - const referencedSeriesAvailable = referencedSeriesInstanceUIDs && - referencedSeriesInstanceUIDs.length !== 0; - if (referencedSeriesAvailable) { - const referencedDisplaySet = otherDisplaySets.find(ds => - referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID) - ); - ; - return referencedDisplaySet; - } -}; - /** * Returns the referenced series instance UIDs by searching the information in the * ReferencedSeriesSequence. @@ -997,6 +998,9 @@ function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence ( const referencedImageArray = _toArray(metadata.ReferencedImageSequence); for (let i = 0; i < referencedImageArray.length; i++) { const { ReferencedSOPInstanceUID } = referencedImageArray[i]; + if (!ReferencedSOPInstanceUID) { + continue; + } referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSOPInstanceUID( displaySets, @@ -1031,8 +1035,10 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence ( const firstFunctionalGroups = _toArray( PerFrameFunctionalGroupsSequence )[0]; - const { DerivationImageSequence } = firstFunctionalGroups; - SourceImageSequence = DerivationImageSequence; + if (firstFunctionalGroups) { + const { DerivationImageSequence } = firstFunctionalGroups; + SourceImageSequence = DerivationImageSequence; + } } if (!SourceImageSequence) { @@ -1071,9 +1077,16 @@ function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID ( for (let i = 0; i < imageSets.length; i++) { const { images } = imageSets[i]; + if (!images) { + continue; + } for (let j = 0; j < images.length; j++) { - if (images[j].SOPInstanceUID === SOPInstanceUID) { - return [images[j].getData().metadata.SeriesInstanceUID]; + const image = images[j]; + if (!image) { + continue; + } + if (image.SOPInstanceUID === SOPInstanceUID) { + return [image.getData().metadata.SeriesInstanceUID]; } } } @@ -1083,4 +1096,4 @@ function _toArray(arrayOrObject) { return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject]; } -export {StudyMetadata, getReferencedDisplaySet}; +export {StudyMetadata}; diff --git a/platform/core/src/utils/isDisplaySetReconstructable.js b/platform/core/src/utils/isDisplaySetReconstructable.js index bf9d72c59..bb583c23c 100644 --- a/platform/core/src/utils/isDisplaySetReconstructable.js +++ b/platform/core/src/utils/isDisplaySetReconstructable.js @@ -6,7 +6,7 @@ import { ReconstructionIssues } from './../enums.js'; * * @param {Object[]} An array of `OHIFInstanceMetadata` objects. * - * @returns {Object} value, warningIssues. + * @returns {Object} value, reconstructionIssues. */ function isDisplaySetReconstructable(instances) { if (!instances.length) { @@ -38,11 +38,11 @@ function isDisplaySetReconstructable(instances) { * Process reconstructable multiframes checks * TODO: deal with multriframe checks! return false for now as can't reconstruct. * * - * @returns {Object} value and warningIssues. + * @returns {Object} value and reconstructionIssues. */ function processMultiframe() { - const warningIssues = [ReconstructionIssues.MULTIFRAMES]; - return { value: false, warningIssues }; + const reconstructionIssues = [ReconstructionIssues.MULTIFRAMES]; + return { value: false, reconstructionIssues }; } /** @@ -50,7 +50,7 @@ function processMultiframe() { * * @param {Object[]} An array of `OHIFInstanceMetadata` objects. * - * @returns {Object} value and warningIssues. + * @returns {Object} value and reconstructionIssues. */ function processSingleframe(instances) { const n = instances.length; @@ -60,7 +60,7 @@ function processSingleframe(instances) { const firstImageSamplesPerPixel = firstImage.SamplesPerPixel; const firstImageOrientationPatient = firstImage.ImageOrientationPatient; - const warningIssues = []; + const reconstructionIssues = []; // Can't reconstruct if we: // -- Have a different dimensions within a displaySet. // -- Have a different number of components within a displaySet. @@ -75,24 +75,24 @@ function processSingleframe(instances) { } = instance; if (Rows !== firstImageRows || Columns !== firstImageColumns) { - warningIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS); + reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS); } else if (SamplesPerPixel !== firstImageSamplesPerPixel) { - warningIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS); + reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS); } else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) { - warningIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION); + reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION); } - if (warningIssues.length !== 0) { + if (reconstructionIssues.length !== 0) { break; } } // check if dataset is 4D if (_isDataset4D(instances)) { - warningIssues.push(ReconstructionIssues.DATASET_4D); + reconstructionIssues.push(ReconstructionIssues.DATASET_4D); } - return { value: warningIssues.length === 0 ? true : false, warningIssues }; + return { value: reconstructionIssues.length === 0 ? true : false, reconstructionIssues }; } /** @@ -102,14 +102,14 @@ function processSingleframe(instances) { * @param {Object[]} An array of `OHIFInstanceMetadata` objects. * @param {boolean} is the dataset 4D. * - * @returns {Object} isUniform, warningIssues and missingFrames + * @returns {Object} isUniform, reconstructionIssues and missingFrames */ function isSpacingUniform(instances, datasetIs4D) { const n = instances.length; const firstImage = instances[0].getData().metadata; const firstImagePositionPatient = firstImage.ImagePositionPatient; - const warningIssues = []; + const reconstructionIssues = []; let missingFrames = 0; // Check if frame spacing is approximately equal within a spacingTolerance. @@ -154,7 +154,7 @@ function isSpacingUniform(instances, datasetIs4D) { if (issue === ReconstructionIssues.MISSING_FRAMES) { missingFrames += spacingIssue.missingFrames; } else if (issue === ReconstructionIssues.IRREGULAR_SPACING) { - warningIssues.push(issue); + reconstructionIssues.push(issue); break; } } @@ -164,7 +164,7 @@ function isSpacingUniform(instances, datasetIs4D) { } } - return { isUniform: warningIssues.length === 0 ? true : false, missingFrames, warningIssues }; + return { isUniform: reconstructionIssues.length === 0 ? true : false, missingFrames, reconstructionIssues }; } diff --git a/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js b/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js index d655f15d6..eaaca285a 100644 --- a/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js +++ b/platform/core/src/utils/loadAndCacheDerivedDisplaySets.js @@ -107,7 +107,12 @@ async function loadAndCacheDerivedDisplaySets(referencedDisplaySet, studies, log }); try { - await recentDisplaySet.load(referencedDisplaySet, studies); + if (recentDisplaySet.hasOwnProperty('getSourceDisplaySet') && + typeof recentDisplaySet.getSourceDisplaySet === 'function') { + await recentDisplaySet.getSourceDisplaySet(studies); + } else { + await recentDisplaySet.load(referencedDisplaySet, studies); + } } catch (error) { recentDisplaySet.isLoaded = false; recentDisplaySet.loadError = true; diff --git a/platform/ui/src/components/studyBrowser/ImageThumbnail.js b/platform/ui/src/components/studyBrowser/ImageThumbnail.js index 98891f279..5f83d7348 100644 --- a/platform/ui/src/components/studyBrowser/ImageThumbnail.js +++ b/platform/ui/src/components/studyBrowser/ImageThumbnail.js @@ -3,6 +3,7 @@ import './ImageThumbnail.styl'; import { utils } from '@ohif/core'; import React, { useState, useEffect, createRef } from 'react'; +import classNames from 'classnames'; import PropTypes from 'prop-types'; import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator'; @@ -15,6 +16,7 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator'; //import cornerstone from 'cornerstone-core'; function ImageThumbnail(props) { const { + active, width, height, imageSrc, @@ -97,7 +99,7 @@ function ImageThumbnail(props) { }, [fetchImagePromise, image.imageId, imageId, purgeCancelablePromise, setImagePromise]); return ( -
    +
    {shouldRenderToCanvas() ? ( @@ -126,6 +128,7 @@ function ImageThumbnail(props) { } ImageThumbnail.propTypes = { + active: PropTypes.bool, imageSrc: PropTypes.string, imageId: PropTypes.string, error: PropTypes.bool, @@ -135,6 +138,7 @@ ImageThumbnail.propTypes = { }; ImageThumbnail.defaultProps = { + active: false, error: false, stackPercentComplete: 0, width: 217, diff --git a/platform/ui/src/components/studyBrowser/ImageThumbnail.styl b/platform/ui/src/components/studyBrowser/ImageThumbnail.styl index dce5c7d41..d23c732ee 100644 --- a/platform/ui/src/components/studyBrowser/ImageThumbnail.styl +++ b/platform/ui/src/components/studyBrowser/ImageThumbnail.styl @@ -6,7 +6,7 @@ --sidebar-transition: all 0.3s ease; } -.ThumbnailEntry.active .ImageThumbnail +.ImageThumbnail.active border-color: var(--active-color); box-shadow: none transition: var(--sidebar-transition); @@ -14,7 +14,7 @@ .ImageThumbnail background-color: var(--primary-background-color); box-shadow: inset 0 0 0 1px var(--ui-border-color-dark); - border: 5px solid transparent + border: 2px solid transparent border-radius: 12px height: 135px margin: 0 auto diff --git a/platform/ui/src/components/studyBrowser/StudyBrowser.js b/platform/ui/src/components/studyBrowser/StudyBrowser.js index 469c74440..0bb75058a 100644 --- a/platform/ui/src/components/studyBrowser/StudyBrowser.js +++ b/platform/ui/src/components/studyBrowser/StudyBrowser.js @@ -20,6 +20,7 @@ function StudyBrowser(props) { return study.thumbnails.map((thumb, thumbIndex) => { // TODO: Thumb has more props than we care about? const { + active, altImageText, displaySetInstanceUID, imageId, @@ -38,6 +39,7 @@ function StudyBrowser(props) { data-cy="thumbnail-list" > { let unmounted = false hasWarnings.then(response => { if (!unmounted) { - warningListSet(response) + inconsistencyWarningsSet(response) } }) return () => { @@ -41,22 +41,22 @@ function ThumbnailFooter({ ); }; - const getWarningContent = (warningList) => { - if (Array.isArray(warningList)) { - const listedWarnings = warningList.map((warn, index) => { + const getWarningContent = (inconsistencyWarnings) => { + if (Array.isArray(inconsistencyWarnings)) { + const listedWarnings = inconsistencyWarnings.map((warn, index) => { return
  • {warn}
  • ; }); return
      {listedWarnings}
    ; } else { - return {warningList}; + return {inconsistencyWarnings}; } }; - const getWarningInfo = (SeriesNumber, warningList) => { + const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => { return( - {warningList.length != 0 ? ( + {inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
    Series Inconsistencies
    -
    {getWarningContent(warningList)}
    +
    {getWarningContent(inconsistencyWarnings)}
    } > @@ -87,7 +87,7 @@ function ThumbnailFooter({ SeriesNumber, InstanceNumber, numImageFrames, - warningList + inconsistencyWarnings ) => { if (!SeriesNumber && !InstanceNumber && !numImageFrames) { return; @@ -97,7 +97,7 @@ function ThumbnailFooter({ {getInfo(SeriesNumber, 'S:')} {getInfo(InstanceNumber, 'I:')} {getInfo(numImageFrames, '', 'image-frames')} - {getWarningInfo(SeriesNumber, warningList)} + {getWarningInfo(SeriesNumber, inconsistencyWarnings)}
    return (seriesInformation); @@ -106,7 +106,7 @@ function ThumbnailFooter({ return (
    {SeriesDescription}
    - {getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, warningList)} + {getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, inconsistencyWarnings)}
    ); } @@ -159,6 +159,7 @@ function Thumbnail(props) { {/* SHOW IMAGE */} {hasImage && ( { ':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .left-mid.orientation-marker' ).as('viewport2InfoMidLeft'); cy.get( - ':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOverlay > div.bottom-right.overlay-element > div' + ':nth-child(2) > .viewport-wrapper > .viewport-element > .OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div' ).as('viewport2InfoBottomRight'); // Press multiples hotkeys on viewport #2 diff --git a/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js b/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js index c0a958059..8ba329af1 100644 --- a/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js +++ b/platform/viewer/cypress/integration/common/OHIFUserPreferences.spec.js @@ -741,7 +741,7 @@ describe('OHIF User Preferences', () => { // Overlay information from 2nd viewport let second_viewport_overlay = - 'div:nth-child(2) > div > div.viewport-element > div.ViewportOverlay > div.bottom-right.overlay-element > div'; + 'div:nth-child(2) > div > div.viewport-element > div.OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div'; // Shift active viewport to Viewport #2 cy.get('body').type('{rightarrow}'); diff --git a/platform/viewer/cypress/support/aliases.js b/platform/viewer/cypress/support/aliases.js index 82c000c01..fb53865ce 100644 --- a/platform/viewer/cypress/support/aliases.js +++ b/platform/viewer/cypress/support/aliases.js @@ -25,10 +25,10 @@ export function initCommonElementsAliases() { '.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton' ).as('seriesBtn'); cy.get('section.sidepanel.from-left').as('seriesPanel'); - cy.get('div.ViewportOverlay > div.bottom-left.overlay-element > div').as( + cy.get('div.OHIFCornerstoneViewportOverlay > div.bottom-left.overlay-element > div').as( 'viewportInfoBottomLeft' ); - cy.get('div.ViewportOverlay > div.bottom-right.overlay-element > div').as( + cy.get('div.OHIFCornerstoneViewportOverlay > div.bottom-right.overlay-element > div').as( 'viewportInfoBottomRight' ); cy.get('.left-mid.orientation-marker').as('viewportInfoMidLeft'); diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js index d22fa8721..c4e2b25d2 100644 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ b/platform/viewer/src/connectedComponents/Viewer.js @@ -197,18 +197,38 @@ class Viewer extends Component { ]); } + const activeViewport = this.props.viewports[this.props.activeViewportIndex]; + const activeDisplaySetInstanceUID = + activeViewport ? activeViewport.displaySetInstanceUID : undefined; this.setState({ - thumbnails: _mapStudiesToThumbnails(studies), + thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID), }); } } componentDidUpdate(prevProps) { - const { studies, isStudyLoaded } = this.props; + 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 + ) { - if (studies !== prevProps.studies) { this.setState({ - thumbnails: _mapStudiesToThumbnails(studies), + thumbnails: _mapStudiesToThumbnails(studies, activeDisplaySetInstanceUID), }); } if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) { @@ -391,56 +411,63 @@ export default withDialog(Viewer); * @returns {[string]} an array of strings containing the warnings */ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studies) { - const warningsList = []; + if (displaySet.inconsistencyWarnings) { + // warnings already checked and cached in displaySet + return displaySet.inconsistencyWarnings; + } + const inconsistencyWarnings = []; if (displaySet.Modality !== 'SEG') { - if (displaySet.warningIssues && displaySet.warningIssues.length !== 0) { - displaySet.warningIssues.forEach(warning => { + if (displaySet.reconstructionIssues && displaySet.reconstructionIssues.length !== 0) { + displaySet.reconstructionIssues.forEach(warning => { switch (warning) { case ReconstructionIssues.DATASET_4D: - warningsList.push('The dataset is 4D.'); + inconsistencyWarnings.push('The dataset is 4D.'); break; case ReconstructionIssues.VARYING_IMAGESDIMENSIONS: - warningsList.push('The dataset frames have different dimensions (rows, columns).'); + inconsistencyWarnings.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).'); + inconsistencyWarnings.push('The dataset frames have different components (Sample per pixel).'); break; case ReconstructionIssues.VARYING_IMAGESORIENTATION: - warningsList.push('The dataset frames have different orientation.'); + inconsistencyWarnings.push('The dataset frames have different orientation.'); break; case ReconstructionIssues.IRREGULAR_SPACING: - warningsList.push('The dataset frames have different pixel spacing.'); + inconsistencyWarnings.push('The dataset frames have different pixel spacing.'); break; case ReconstructionIssues.MULTIFFRAMES: - warningsList.push('The dataset is a multiframes.'); + inconsistencyWarnings.push('The dataset is a multiframes.'); break; default: break; } }); - warningsList.push('The datasets is not a reconstructable 3D volume. MPR mode is not available.'); + inconsistencyWarnings.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 + '.'); + (!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) { - return warningsList; + displaySet.inconsistencyWarnings = inconsistencyWarnings; + return inconsistencyWarnings; } const { referencedDisplaySet } = displaySet.getSourceDisplaySet(studies, false); if (!referencedDisplaySet) { - return warningsList; + displaySet.inconsistencyWarnings = inconsistencyWarnings; + return inconsistencyWarnings; } const imageIds = referencedDisplaySet.images.map(image => image.getImageId()); if (!imageIds || imageIds.length === 0) { - return warningsList; + displaySet.inconsistencyWarnings = inconsistencyWarnings; + return inconsistencyWarnings; } for ( @@ -462,10 +489,10 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie .SourceImageSequence; } if (!SourceImageSequence) { - if (warningsList.length === 0) { + if (inconsistencyWarnings.length === 0) { const warningMessage = 'The segmentation ' + 'has frames out of plane respect to the source images.'; - warningsList.push(warningMessage); + inconsistencyWarnings.push(warningMessage); } continue; } @@ -501,21 +528,70 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie const warningMessage = 'The segmentation ' + 'has frames with different geometry ' + 'dimensions (Rows and Columns) respect to the source images.'; - warningsList.push(warningMessage); + inconsistencyWarnings.push(warningMessage); break; } } - if (warningsList.length !== 0) { + if (inconsistencyWarnings.length !== 0) { const warningMessage = 'The segmentation format is not supported yet. ' + 'The segmentation data (segments) could not be loaded.'; - warningsList.push(warningMessage); + inconsistencyWarnings.push(warningMessage); } } - return warningsList; + // 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; +}; + /** * What types are these? Why do we have "mapping" dropped in here instead of in * a mapping layer? @@ -524,12 +600,11 @@ const _checkForSeriesInconsistencesWarnings = async function (displaySet, studie * - Add showStackLoadingProgressBar option * * @param {Study[]} studies - * @param {DisplaySet[]} studies[].displaySets + * @param {string} activeDisplaySetInstanceUID */ -const _mapStudiesToThumbnails = function(studies) { +const _mapStudiesToThumbnails = function(studies, activeDisplaySetInstanceUID) { return studies.map(study => { const { StudyInstanceUID } = study; - const thumbnails = study.displaySets.map(displaySet => { const { displaySetInstanceUID, @@ -549,15 +624,16 @@ const _mapStudiesToThumbnails = function(studies) { 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); + const active = _isDisplaySetActive(displaySet, studies, activeDisplaySetInstanceUID) return { + active, imageId, altImageText, displaySetInstanceUID,