diff --git a/extensions/vtk/src/toolbarComponents/VTKMPRToolbarButton.js b/extensions/vtk/src/toolbarComponents/VTKMPRToolbarButton.js index 333c57c34..82ebbc5e6 100644 --- a/extensions/vtk/src/toolbarComponents/VTKMPRToolbarButton.js +++ b/extensions/vtk/src/toolbarComponents/VTKMPRToolbarButton.js @@ -1,18 +1,21 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { ToolbarButton } from '@ohif/ui'; import { utils } from '@ohif/core'; -import { createSelector } from 'reselect'; + const { studyMetadataManager } = utils; -const _isDisplaySetReconstructable = ( - displaySetInstanceUID = '', - StudyInstanceUID = '' -) => { - if (displaySetInstanceUID == '' || StudyInstanceUID == '') { +let isVisible = true; + +const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => { + if (!viewportSpecificData[activeViewportIndex]) { return false; - } + }; + + const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[ + activeViewportIndex + ]; const studies = studyMetadataManager.all(); @@ -24,132 +27,38 @@ const _isDisplaySetReconstructable = ( return false; } - const displaySet = study._displaySets.find( - set => set.displaySetInstanceUID === displaySetInstanceUID - ); + const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID); if (!displaySet) { return false; - } - - // 2D MPR is not currently available for 4D datasets. - - // Assuming that slices at different time have the same position, here we just check if - // there are multiple slices for the same ImagePositionPatient and disable MPR. - - // A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values. - // However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags. - - for (let ii = 0; ii < displaySet.numImageFrames; ++ii) { - const image = displaySet.images[ii]; - if (!image) continue; - - const imageIdControl = image.getImageId(); - const instanceMetadataControl = cornerstone.metaData.get( - 'instance', - imageIdControl - ); - - if ( - !instanceMetadataControl || - instanceMetadataControl === undefined || - !instanceMetadataControl.ImagePositionPatient || - instanceMetadataControl.ImagePositionPatient === undefined - ) { - // if ImagePositionPatient is missing, skip the 4D datasets check. - // do not return false, because it could be a 3D dataset. - continue; - } - - let xImagePositionPatientControl = - instanceMetadataControl.ImagePositionPatient[0]; - let yImagePositionPatientControl = - instanceMetadataControl.ImagePositionPatient[1]; - let zImagePositionPatientControl = - instanceMetadataControl.ImagePositionPatient[2]; - - for (let jj = ii + 1; jj < displaySet.numImageFrames; ++jj) { - const image = displaySet.images[jj]; - if (!image) continue; - - const imageId = image.getImageId(); - const instanceMetadata = cornerstone.metaData.get('instance', imageId); - - if ( - !instanceMetadata || - instanceMetadata === undefined || - !instanceMetadata.ImagePositionPatient || - instanceMetadata.ImagePositionPatient === undefined - ) { - // if ImagePositionPatient is missing, skip the 4D datasets check. - // do not return false, because it could be a 3D dataset. - continue; - } - - let xImagePositionPatient = instanceMetadata.ImagePositionPatient[0]; - let yImagePositionPatient = instanceMetadata.ImagePositionPatient[1]; - let zImagePositionPatient = instanceMetadata.ImagePositionPatient[2]; - - // ImagePositionPatient is float - if ( - Math.abs( xImagePositionPatientControl - xImagePositionPatient ) < 1e-6 && - Math.abs( yImagePositionPatientControl - yImagePositionPatient ) < 1e-6 && - Math.abs( zImagePositionPatientControl - zImagePositionPatient ) < 1e-6 - ) { - return false; - } - } - } + }; return displaySet.isReconstructable; }; -const selectDisplaySetInstanceUID = state => { - if ( - state.viewports.viewportSpecificData[state.viewports.activeViewportIndex] - ) { - return state.viewports.viewportSpecificData[ - state.viewports.activeViewportIndex - ].displaySetInstanceUID; - } else { - return ''; - } -}; - -const selectStudyInstanceUID = state => { - if ( - state.viewports.viewportSpecificData[state.viewports.activeViewportIndex] - ) { - return state.viewports.viewportSpecificData[ - state.viewports.activeViewportIndex - ].StudyInstanceUID; - } else { - return ''; - } -}; - -const stateSelector = createSelector( - [selectDisplaySetInstanceUID, selectStudyInstanceUID], - (displaySetInstanceUID, StudyInstanceUID) => { - return { - displaySetInstanceUID, - StudyInstanceUID, - }; - } -); - -function VTKMPRToolbarButton({ toolbarClickCallback, button, isActive }) { +function VTKMPRToolbarButton({ + parentContext, + toolbarClickCallback, + button, + activeButtons, + isActive, + className, +}) { const { id, label, icon } = button; - const { displaySetInstanceUID, StudyInstanceUID } = useSelector( - stateSelector - ); + const { viewportSpecificData, activeViewportIndex } = useSelector(state => { + const { viewports = {} } = state; + const { viewportSpecificData, activeViewportIndex } = viewports; - const isVisible = useMemo(() => { - return _isDisplaySetReconstructable( - displaySetInstanceUID, - StudyInstanceUID - ); - }, [displaySetInstanceUID, StudyInstanceUID]); + return { + viewportSpecificData, + activeViewportIndex, + } + }); + + isVisible = _isDisplaySetReconstructable( + viewportSpecificData, + activeViewportIndex, + ); return ( diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js index 06810710b..0dd12f791 100644 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ b/platform/core/src/classes/metadata/StudyMetadata.js @@ -767,6 +767,16 @@ const isMultiFrame = instance => { return instance.getTagValue('NumberOfFrames') > 1; }; +/** + * Creates a display set for a series. + * Checks if a series is reconstructable to a 3D volume. + * If reconstructable, the frames are sorted. + * + * @param {SeriesMetadata} series The series metadata object from which the display sets will be created + * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. + * + * @returns {Object} imageSet. + */ const makeDisplaySet = (series, instances) => { const instance = instances[0]; const imageSet = new ImageSet(instances); @@ -804,18 +814,22 @@ const makeDisplaySet = (series, instances) => { imageSet.getImage(0).getTagValue('InstanceNumber') ); - const isReconstructable = isDisplaySetReconstructable(instances); - - imageSet.isReconstructable = isReconstructable.value; + const displayReconstructableInfo = isDisplaySetReconstructable(instances); + imageSet.isReconstructable = displayReconstructableInfo.value; if (shallSort && imageSet.isReconstructable) { imageSet.sortByImagePositionPatient(); } - if (isReconstructable.missingFrames) { + if (displayReconstructableInfo.missingFrames) { // TODO -> This is currently unused, but may be used for reconstructing // Volumes with gaps later on. - imageSet.missingFrames = isReconstructable.missingFrames; + imageSet.missingFrames = displayReconstructableInfo.missingFrames; + } + + if (!imageSet.displayReconstructableInfo) { + // It is not reconstrabale Save type of warning + imageSet.warningIssues = displayReconstructableInfo.warningIssues; } return imageSet; diff --git a/platform/core/src/enums.js b/platform/core/src/enums.js new file mode 100644 index 000000000..c3721e518 --- /dev/null +++ b/platform/core/src/enums.js @@ -0,0 +1,11 @@ +const ReconstructionIssues = { + DATASET_4D: 'datasetis4D', + VARYING_IMAGESDIMENSIONS: 'imagesdimensionsvarying', + VARYING_IMAGESCOMPONENTS: 'imagescomponentsvarying', + VARYING_IMAGESORIENTATION: 'imagesorientationvarying', + MISSING_FRAMES: 'missingframes', + IRREGULAR_SPACING: 'irregularspacing', + MULTIFFRAMES: 'multiframe', +}; + +export {ReconstructionIssues}; diff --git a/platform/core/src/utils/isDisplaySetReconstructable.js b/platform/core/src/utils/isDisplaySetReconstructable.js index 0f5912706..31855964b 100644 --- a/platform/core/src/utils/isDisplaySetReconstructable.js +++ b/platform/core/src/utils/isDisplaySetReconstructable.js @@ -1,7 +1,12 @@ +import { ReconstructionIssues } from './../enums.js'; + + /** * Checks if a series is reconstructable to a 3D volume. * * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. + * + * @returns {Object} reconstructable value, missingFrames and warningIssues. */ export default function isDisplaySetReconstructable(instances) { if (!instances.length) { @@ -29,12 +34,28 @@ export default function isDisplaySetReconstructable(instances) { } } +/** + * Process reconstructable multiframes checks + * TODO: deal with multriframe checks! return false for now as can't reconstruct. + * + * @param {Object} instanceof `OHIFInstanceMetadata` objects. Currently not used. + * + * @returns {Object} reconstructable value and warningIssues. + */ function processMultiframe(instance) { - //TODO: deal with multriframe checks! return false for now as can't reconstruct. - return { value: false }; + const warningIssues = [ReconstructionIssues.MULTIFRAMES]; + return { value: false, warningIssues }; } +/** + * Process reconstructable single frame checks + * + * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. + * + * @returns {Object} reconstructable value, missingFrames and warningIssues. + */ function processSingleframe(instances) { + const n = instances.length; const firstImage = instances[0].getData().metadata; const firstImageRows = firstImage.Rows; const firstImageColumns = firstImage.Columns; @@ -42,12 +63,13 @@ function processSingleframe(instances) { const firstImageOrientationPatient = firstImage.ImageOrientationPatient; const firstImagePositionPatient = firstImage.ImagePositionPatient; + const warningIssues = []; // Can't reconstruct if we: // -- Have a different dimensions within a displaySet. // -- Have a different number of components within a displaySet. // -- Have different orientations within a displaySet. - for (let i = 1; i < instances.length; i++) { - const instance = instances[i].getData().metadata; + for (let ii = 1; ii < n; ++ii) { + const instance = instances[ii].getData().metadata; const { Rows, Columns, @@ -55,13 +77,16 @@ function processSingleframe(instances) { ImageOrientationPatient, } = instance; - if ( - Rows !== firstImageRows || - Columns !== firstImageColumns || - SamplesPerPixel !== firstImageSamplesPerPixel || - !_isSameOrientation(ImageOrientationPatient, firstImageOrientationPatient) - ) { - return { value: false }; + if (Rows !== firstImageRows || Columns !== firstImageColumns) { + warningIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS); + } else if (SamplesPerPixel !== firstImageSamplesPerPixel) { + warningIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS); + } else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) { + warningIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION); + } + + if (warningIssues.length !== 0) { + break; } } @@ -70,52 +95,101 @@ function processSingleframe(instances) { // Check if frame spacing is approximately equal within a spacingTolerance. // If spacing is on a uniform grid but we are missing frames, // Allow reconstruction, but pass back the number of missing frames. - if (instances.length > 2) { - const lastIpp = instances[instances.length - 1].getData().metadata + if (n > 2) { + const lastIpp = instances[n - 1].getData().metadata .ImagePositionPatient; // We can't reconstruct if we are missing ImagePositionPatient values - if (!firstImagePositionPatient || !lastIpp) { - return { value: false }; - } + if (firstImagePositionPatient && lastIpp) { + const averageSpacingBetweenFrames = + _getPerpendicularDistance(firstImagePositionPatient, lastIpp) / + (n - 1); - const averageSpacingBetweenFrames = - _getPerpendicularDistance(firstImagePositionPatient, lastIpp) / - (instances.length - 1); + let previousImagePositionPatient = firstImagePositionPatient; - let previousImagePositionPatient = firstImagePositionPatient; + for (let ii = 1; ii < n; ++ii) { + const instance = instances[ii].getData().metadata; + const { ImagePositionPatient } = instance; - for (let i = 1; i < instances.length; i++) { - const instance = instances[i].getData().metadata; - const { ImagePositionPatient } = instance; + const spacingBetweenFrames = _getPerpendicularDistance( + ImagePositionPatient, + previousImagePositionPatient + ); + const spacingIssue = _getSpacingIssue( + spacingBetweenFrames, + averageSpacingBetweenFrames + ); - const spacingBetweenFrames = _getPerpendicularDistance( - ImagePositionPatient, - previousImagePositionPatient - ); - const spacingIssue = _getSpacingIssue( - spacingBetweenFrames, - averageSpacingBetweenFrames - ); + if (spacingIssue) { + const issue = spacingIssue.issue; - if (spacingIssue) { - const issue = spacingIssue.issue; - - if (issue === reconstructionIssues.MISSING_FRAMES) { - missingFrames += spacingIssue.missingFrames; - } else if (issue === reconstructionIssues.IRREGULAR_SPACING) { - return { value: false }; + if (issue === ReconstructionIssues.MISSING_FRAMES) { + missingFrames += spacingIssue.missingFrames; + } else if (issue === ReconstructionIssues.IRREGULAR_SPACING) { + warningIssues.push(issue); + break; + } } - } - previousImagePositionPatient = ImagePositionPatient; + previousImagePositionPatient = ImagePositionPatient; + } } } - return { value: true, missingFrames }; + // check if dataset is 4D + if (_isDataset4D(instances)) { + warningIssues.push(ReconstructionIssues.DATASET_4D); + } + + return { value: warningIssues.length === 0 ? true : false, missingFrames, warningIssues }; } -function _isSameOrientation(iop1, iop2) { +/** + * Check if 4D dataset. + * + * Assuming that slices at different time have the same position, here we just check if + * there are multiple slices for the same ImagePositionPatient and disable MPR. + * + * A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values. + * However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags. + * + * @param {Object[]} instances An array of `OHIFInstanceMetadata` objects. + * + * @returns {boolean} reconstructable value. + */ + function _isDataset4D(instances) { + const n = instances.length; + for (let ii = 0; ii < n; ++ii) { + const instanceMetadataControl = instances[ii].getData().metadata; + if ( + !instanceMetadataControl || + instanceMetadataControl === undefined || + !instanceMetadataControl.ImagePositionPatient || + instanceMetadataControl.ImagePositionPatient === undefined + ) { + continue; + } + for (let jj = ii + 1; jj < n; ++jj) { + const instanceMetadata = instances[jj].getData().metadata; + if ( + !instanceMetadata || + instanceMetadata === undefined || + !instanceMetadata.ImagePositionPatient || + instanceMetadata.ImagePositionPatient === undefined + ) { + continue; + } + + if (_isSameArray(instanceMetadataControl.ImagePositionPatient, instanceMetadata.ImagePositionPatient)) { + return true; + } + } + } + + return false; +} + +function _isSameArray(iop1, iop2) { if (iop1 === undefined || !iop2 === undefined) { return; } @@ -156,12 +230,12 @@ function _getSpacingIssue(spacing, averageSpacing) { if (errorForEachSpacing < spacingTolerance * averageSpacing) { return { - issue: reconstructionIssues.MISSING_FRAMES, + issue: ReconstructionIssues.MISSING_FRAMES, missingFrames: numberOfSpacings - 1, }; } - return { issue: reconstructionIssues.IRREGULAR_SPACING }; + return { issue: ReconstructionIssues.IRREGULAR_SPACING }; } function _getPerpendicularDistance(a, b) { @@ -173,7 +247,3 @@ function _getPerpendicularDistance(a, b) { } const constructableModalities = ['MR', 'CT', 'PT', 'NM']; -const reconstructionIssues = { - MISSING_FRAMES: 'missingframes', - IRREGULAR_SPACING: 'irregularspacing', -}; diff --git a/platform/ui/src/components/studyBrowser/StudyBrowser.js b/platform/ui/src/components/studyBrowser/StudyBrowser.js index 921888807..469c74440 100644 --- a/platform/ui/src/components/studyBrowser/StudyBrowser.js +++ b/platform/ui/src/components/studyBrowser/StudyBrowser.js @@ -28,6 +28,7 @@ function StudyBrowser(props) { SeriesDescription, SeriesNumber, stackPercentComplete, + hasWarnings, } = thumb; return ( @@ -50,6 +51,7 @@ function StudyBrowser(props) { numImageFrames={numImageFrames} SeriesDescription={SeriesDescription} SeriesNumber={SeriesNumber} + hasWarnings={hasWarnings} stackPercentComplete={stackPercentComplete} // Events onClick={onThumbnailClick.bind( diff --git a/platform/ui/src/components/studyBrowser/Thumbnail.js b/platform/ui/src/components/studyBrowser/Thumbnail.js index 66b102560..fe9b72bf8 100644 --- a/platform/ui/src/components/studyBrowser/Thumbnail.js +++ b/platform/ui/src/components/studyBrowser/Thumbnail.js @@ -1,8 +1,11 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { useDrag } from 'react-dnd'; import ImageThumbnail from './ImageThumbnail'; import classNames from 'classnames'; +import { Icon } from './../../elements/Icon'; +import { Tooltip } from './../tooltip'; +import { OverlayTrigger } from './../overlayTrigger'; import './Thumbnail.styl'; @@ -11,7 +14,22 @@ function ThumbnailFooter({ SeriesNumber, InstanceNumber, numImageFrames, + hasWarnings }) { + const [warningList, warningListSet] = useState([]); + + useEffect(() => { + let unmounted = false + hasWarnings.then(response => { + if (!unmounted) { + warningListSet(response) + } + }) + return () => { + unmounted = true + } + }, []) + const infoOnly = !SeriesDescription; const getInfo = (value, icon, className = '') => { @@ -22,28 +40,73 @@ function ThumbnailFooter({ ); }; + + const getWarningContent = (warningList) => { + if (Array.isArray(warningList)) { + const listedWarnings = warningList.map((warn, index) => { + return
  • {warn}
  • ; + }); + + return
      {listedWarnings}
    ; + } else { + return {warningList}; + } + }; + + const getWarningInfo = (SeriesNumber, warningList) => { + return( + + {warningList.length != 0 ? ( + +
    Series Inconsistencies
    +
    {getWarningContent(warningList)}
    + + } + > +
    + + + +
    +
    + ) : ( + + )} +
    + ); + }; const getSeriesInformation = ( SeriesNumber, InstanceNumber, - numImageFrames + numImageFrames, + warningList ) => { if (!SeriesNumber && !InstanceNumber && !numImageFrames) { return; } - - return ( + const seriesInformation =
    {getInfo(SeriesNumber, 'S:')} {getInfo(InstanceNumber, 'I:')} {getInfo(numImageFrames, '', 'image-frames')} + {getWarningInfo(SeriesNumber, warningList)}
    - ); + + return (seriesInformation); }; return (
    {SeriesDescription}
    - {getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames)} + {getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, warningList)}
    ); } @@ -60,6 +123,7 @@ function Thumbnail(props) { numImageFrames, SeriesDescription, SeriesNumber, + hasWarnings, stackPercentComplete, StudyInstanceUID, onClick, @@ -126,13 +190,14 @@ Thumbnail.propTypes = { stackPercentComplete: PropTypes.number, /** altImageText will be used when no imageId or imageSrc is provided. -It will be displayed inside the
    . This is useful when it is difficult + It will be displayed inside the
    . This is useful when it is difficult to make a preview for a type of DICOM series (e.g. DICOM-SR) */ altImageText: PropTypes.string, SeriesDescription: PropTypes.string, SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + hasWarnings: PropTypes.instanceOf(Promise), numImageFrames: PropTypes.number, onDoubleClick: PropTypes.func, onClick: PropTypes.func, diff --git a/platform/ui/src/components/studyBrowser/Thumbnail.styl b/platform/ui/src/components/studyBrowser/Thumbnail.styl index 47e6ecb8e..904b8319a 100644 --- a/platform/ui/src/components/studyBrowser/Thumbnail.styl +++ b/platform/ui/src/components/studyBrowser/Thumbnail.styl @@ -78,6 +78,17 @@ height: 11px width: 11px + .warning + margin: auto 0; + opacity: 1 + color: #e29e4a; + + svg + width: 16px; + height: 14px; + pointer-events: inherit; + + .value color: var(--text-secondary-color); display: inline-block diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js index 141195949..0eaadc847 100644 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ b/platform/viewer/src/connectedComponents/Viewer.js @@ -12,6 +12,7 @@ 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'; // Contexts import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; @@ -367,6 +368,62 @@ class Viewer extends Component { export default withDialog(Viewer); +/** + * Async function to check if there are any inconsistences in the series + * (i.e. reconstructable to a 3D volume). If not reconstructable MPR is disabled. + * The actual computations are done in isDisplaySetReconstructable. + * + * 1) Is series multiframe? + * 2) Do the frames have different dimensions/numer of components/orientations? + * 3) Has the series any missing frames or irregular spacing? + * 4) Is the series 4D? + * + * @param {*object} displaySet + * @returns {[string]} an array of strings containing the warnings + */ +const _checkForSeriesInconsistencesWarnings = async function (displaySet) { + // NOTE: at the moment this function is async even if it does not perfom any heavy calculation. + // We may add or move here some of the computations + // done when creating the displaySet (see makeDisplaySet and isDisplaySetReconstructable). + // the thumbnail footnotes warning react element is already set up to handle a promise. + const warningsList = []; + 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 + '.'); + } + + return warningsList +} + /** * What types are these? Why do we have "mapping" dropped in here instead of in * a mapping layer? @@ -406,6 +463,8 @@ const _mapStudiesToThumbnails = function(studies) { altImageText = displaySet.Modality ? displaySet.Modality : 'UN'; } + const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet) + return { imageId, altImageText, @@ -414,6 +473,7 @@ const _mapStudiesToThumbnails = function(studies) { InstanceNumber, numImageFrames, SeriesNumber, + hasWarnings, }; });