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
This commit is contained in:
Davide Punzo 2021-05-17 18:48:32 +02:00 committed by GitHub
parent a57ab4d12a
commit 78f1a7f3e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 598 additions and 161 deletions

View File

@ -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 <OHIFCornerstoneViewportOverlay {...props} inconsistencyWarnings={inconsistencyWarnings} />
};
return (
<>
<ConnectedCornerstoneViewport
@ -237,6 +243,7 @@ class OHIFCornerstoneViewport extends Component {
imageIdIndex={currentImageIdIndex}
onNewImageDebounced={newImageHandler}
onNewImageDebounceTime={300}
viewportOverlayComponent={warningsOverlay}
// ~~ Connected (From REDUX)
// frameRate={frameRate}
// isPlaying={false}

View File

@ -0,0 +1,69 @@
.imageViewerViewport.empty ~ .OHIFCornerstoneViewportOverlay {
display: none;
}
.OHIFCornerstoneViewportOverlay {
color: #9ccef9;
}
.OHIFCornerstoneViewportOverlay .overlay-element {
position: absolute;
font-weight: 400;
text-shadow: 1px 1px #000;
pointer-events: none;
}
.OHIFCornerstoneViewportOverlay .top-left {
top: 20px;
left: 20px;
}
.OHIFCornerstoneViewportOverlay .top-center {
top: 20px;
padding-top: 20px;
width: 100%;
text-align: center;
}
.OHIFCornerstoneViewportOverlay .top-right {
top: 20px;
right: 20px;
text-align: right;
}
.OHIFCornerstoneViewportOverlay .bottom-left {
bottom: 20px;
left: 20px;
}
.OHIFCornerstoneViewportOverlay .bottom-left2 {
bottom: 140px;
left: 20px;
}
.OHIFCornerstoneViewportOverlay .bottom-right {
bottom: 20px;
right: 20px;
text-align: right;
}
.OHIFCornerstoneViewportOverlay.controlsVisible .topright,
.OHIFCornerstoneViewportOverlay.controlsVisible .bottomright {
right: calc(20px + 19px);
}
.OHIFCornerstoneViewportOverlay .warning {
position: absolute;
font-weight: 400;
text-shadow: 1px 1px #000;
pointer-events: inherit;
}
.OHIFCornerstoneViewportOverlay .warning svg {
opacity: 1;
color: #e29e4a;
fill: #e29e4a;
stroke: #e29e4a;
width: 32px;
height: 28px;
}
.OHIFCornerstoneViewportOverlay svg {
color: #9ccef9;
fill: #9ccef9;
stroke: #9ccef9;
background-color: transparent;
margin: 2px;
width: 18px;
height: 18px;
}

View File

@ -0,0 +1,167 @@
import { PureComponent } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import './OHIFCornerstoneViewportOverlay.css';
import {
isValidNumber,
formatNumberPrecision,
formatDICOMDate,
formatDICOMTime,
formatPN,
getCompression
} from '../utils/formatStudy';
import classNames from 'classnames';
import { Icon } from '@ohif/ui/src/elements/Icon';
import { Tooltip } from '@ohif/ui/src/components/tooltip';
import { OverlayTrigger } from '@ohif/ui/src/components/overlayTrigger';
class OHIFCornerstoneViewportOverlay extends PureComponent {
static propTypes = {
scale: PropTypes.number.isRequired,
windowWidth: PropTypes.oneOfType([
PropTypes.number.isRequired,
PropTypes.string.isRequired,
]),
windowCenter: PropTypes.oneOfType([
PropTypes.number.isRequired,
PropTypes.string.isRequired,
]),
imageId: PropTypes.string.isRequired,
imageIndex: PropTypes.number.isRequired,
stackSize: PropTypes.number.isRequired,
inconsistencyWarnings: PropTypes.array.isRequired
};
render() {
const { imageId, scale, windowWidth, windowCenter, inconsistencyWarnings } = this.props;
if (!imageId) {
return null;
}
const zoomPercentage = formatNumberPrecision(scale * 100, 0);
const seriesMetadata =
cornerstone.metaData.get('generalSeriesModule', imageId) || {};
const imagePlaneModule =
cornerstone.metaData.get('imagePlaneModule', imageId) || {};
const { rows, columns, sliceThickness, sliceLocation } = imagePlaneModule;
const { seriesNumber, seriesDescription } = seriesMetadata;
const generalStudyModule =
cornerstone.metaData.get('generalStudyModule', imageId) || {};
const { studyDate, studyTime, studyDescription } = generalStudyModule;
const patientModule =
cornerstone.metaData.get('patientModule', imageId) || {};
const { patientId, patientName } = patientModule;
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
const cineModule = cornerstone.metaData.get('cineModule', imageId) || {};
const { frameTime } = cineModule;
const frameRate = formatNumberPrecision(1000 / frameTime, 1);
const compression = getCompression(imageId);
const wwwc = `W: ${
windowWidth.toFixed ? windowWidth.toFixed(0) : windowWidth
} L: ${windowWidth.toFixed ? windowCenter.toFixed(0) : windowCenter}`;
const imageDimensions = `${columns} x ${rows}`;
const { imageIndex, stackSize } = this.props;
const inconsistencyWarningsOn = inconsistencyWarnings && inconsistencyWarnings.length !== 0 ? true : false;
const getWarningContent = (warningList) => {
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
return <li key={index}>{warn}</li>;
});
return <ol>{listedWarnings}</ol>;
} else {
return <React.Fragment>{warningList}</React.Fragment>;
}
};
const getWarningInfo = (seriesNumber, inconsistencyWarnings) => {
return(
<React.Fragment>
{inconsistencyWarnings.length != 0 ? (
<OverlayTrigger
key={seriesNumber}
placement="left"
overlay={
<Tooltip
placement="left"
className="in tooltip-warning"
id="tooltip-left"
>
<div className="warningTitle">Series Inconsistencies</div>
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
</Tooltip>
}
>
<div className={classNames('warning')}>
<span className="warning-icon">
<Icon name="exclamation-triangle" />
</span>
</div>
</OverlayTrigger>
) : (
<React.Fragment></React.Fragment>
)}
</React.Fragment>
);
};
const normal = (
<React.Fragment>
<div className="top-left overlay-element">
<div>{formatPN(patientName)}</div>
<div>{patientId}</div>
</div>
<div className="top-right overlay-element">
<div>{studyDescription}</div>
<div>
{formatDICOMDate(studyDate)} {formatDICOMTime(studyTime)}
</div>
</div>
<div className="bottom-right overlay-element">
<div>Zoom: {zoomPercentage}%</div>
<div>{wwwc}</div>
<div className="compressionIndicator">{compression}</div>
</div>
<div className="bottom-left2 warning">
<div>{inconsistencyWarningsOn ? getWarningInfo(seriesNumber, inconsistencyWarnings) : ''}</div>
</div>
<div className="bottom-left overlay-element">
<div>{seriesNumber >= 0 ? `Ser: ${seriesNumber}` : ''}</div>
<div>
{stackSize > 1
? `Img: ${instanceNumber} ${imageIndex}/${stackSize}`
: ''}
</div>
<div>
{frameRate >= 0 ? `${formatNumberPrecision(frameRate, 2)} FPS` : ''}
<div>{imageDimensions}</div>
<div>
{isValidNumber(sliceLocation)
? `Loc: ${formatNumberPrecision(sliceLocation, 2)} mm `
: ''}
{sliceThickness
? `Thick: ${formatNumberPrecision(sliceThickness, 2)} mm`
: ''}
</div>
<div>{seriesDescription}</div>
</div>
</div>
</React.Fragment>
);
return <div className="OHIFCornerstoneViewportOverlay">{normal}</div>;
}
}
export default OHIFCornerstoneViewportOverlay;

View File

@ -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
};

View File

@ -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) {

View File

@ -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;
};

View File

@ -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) {

View File

@ -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};

View File

@ -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 };
}

View File

@ -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;

View File

@ -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 (
<div className="ImageThumbnail">
<div className={classNames('ImageThumbnail', { active: active })}>
<div className="image-thumbnail-canvas">
{shouldRenderToCanvas() ? (
<canvas ref={canvasRef} width={width} height={height} />
@ -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,

View File

@ -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

View File

@ -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"
>
<Thumbnail
active={active}
supportsDrag={supportsDrag}
key={`${studyIndex}_${thumbIndex}`}
id={`${studyIndex}_${thumbIndex}`} // Unused?

View File

@ -16,13 +16,13 @@ function ThumbnailFooter({
numImageFrames,
hasWarnings
}) {
const [warningList, warningListSet] = useState([]);
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
useEffect(() => {
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 <li key={index}>{warn}</li>;
});
return <ol>{listedWarnings}</ol>;
} else {
return <React.Fragment>{warningList}</React.Fragment>;
return <React.Fragment>{inconsistencyWarnings}</React.Fragment>;
}
};
const getWarningInfo = (SeriesNumber, warningList) => {
const getWarningInfo = (SeriesNumber, inconsistencyWarnings) => {
return(
<React.Fragment>
{warningList.length != 0 ? (
{inconsistencyWarnings && inconsistencyWarnings.length != 0 ? (
<OverlayTrigger
key={SeriesNumber}
placement="left"
@ -67,7 +67,7 @@ function ThumbnailFooter({
id="tooltip-left"
>
<div className="warningTitle">Series Inconsistencies</div>
<div className="warningContent">{getWarningContent(warningList)}</div>
<div className="warningContent">{getWarningContent(inconsistencyWarnings)}</div>
</Tooltip>
}
>
@ -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)}
</div>
return (seriesInformation);
@ -106,7 +106,7 @@ function ThumbnailFooter({
return (
<div className={classNames('series-details', { 'info-only': infoOnly })}>
<div className="series-description">{SeriesDescription}</div>
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, warningList)}
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, inconsistencyWarnings)}
</div>
);
}
@ -159,6 +159,7 @@ function Thumbnail(props) {
{/* SHOW IMAGE */}
{hasImage && (
<ImageThumbnail
active={active}
imageSrc={imageSrc}
imageId={imageId}
error={error}

View File

@ -2,8 +2,15 @@
:root {
--series-count-background-color: #678696;
--active-color: #20A5D6;
}
.thumbnail.active
.alt-image-text
border-color: var(--active-color);
box-shadow: none
transition: var(--sidebar-transition);
.thumbnail
cursor: pointer
display: table
@ -16,7 +23,7 @@
justify-content: center;
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;

View File

@ -3,6 +3,7 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [4.9.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.9.15...@ohif/viewer@4.9.16) (2021-05-14)
**Note:** Version bump only for package @ohif/viewer

View File

@ -91,7 +91,7 @@ describe('OHIF Cornerstone Hotkeys', () => {
':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

View File

@ -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}');

View File

@ -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');

View File

@ -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,