IDC 2259: add warning UI for series inconsistencies (#2331)
IDC 2259: add warning UI for series inconsistencies remove VTKMPRToolbarButton component memoization (it is not anymore necessary)
This commit is contained in:
parent
db3b15e537
commit
0456d92462
@ -1,18 +1,21 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ToolbarButton } from '@ohif/ui';
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
import { utils } from '@ohif/core';
|
import { utils } from '@ohif/core';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
const { studyMetadataManager } = utils;
|
const { studyMetadataManager } = utils;
|
||||||
|
|
||||||
const _isDisplaySetReconstructable = (
|
let isVisible = true;
|
||||||
displaySetInstanceUID = '',
|
|
||||||
StudyInstanceUID = ''
|
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
|
||||||
) => {
|
if (!viewportSpecificData[activeViewportIndex]) {
|
||||||
if (displaySetInstanceUID == '' || StudyInstanceUID == '') {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
|
||||||
|
activeViewportIndex
|
||||||
|
];
|
||||||
|
|
||||||
const studies = studyMetadataManager.all();
|
const studies = studyMetadataManager.all();
|
||||||
|
|
||||||
@ -24,132 +27,38 @@ const _isDisplaySetReconstructable = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displaySet = study._displaySets.find(
|
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
|
||||||
set => set.displaySetInstanceUID === displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!displaySet) {
|
if (!displaySet) {
|
||||||
return false;
|
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;
|
return displaySet.isReconstructable;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectDisplaySetInstanceUID = state => {
|
function VTKMPRToolbarButton({
|
||||||
if (
|
parentContext,
|
||||||
state.viewports.viewportSpecificData[state.viewports.activeViewportIndex]
|
toolbarClickCallback,
|
||||||
) {
|
button,
|
||||||
return state.viewports.viewportSpecificData[
|
activeButtons,
|
||||||
state.viewports.activeViewportIndex
|
isActive,
|
||||||
].displaySetInstanceUID;
|
className,
|
||||||
} 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 }) {
|
|
||||||
const { id, label, icon } = button;
|
const { id, label, icon } = button;
|
||||||
const { displaySetInstanceUID, StudyInstanceUID } = useSelector(
|
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
|
||||||
stateSelector
|
const { viewports = {} } = state;
|
||||||
);
|
const { viewportSpecificData, activeViewportIndex } = viewports;
|
||||||
|
|
||||||
const isVisible = useMemo(() => {
|
return {
|
||||||
return _isDisplaySetReconstructable(
|
viewportSpecificData,
|
||||||
displaySetInstanceUID,
|
activeViewportIndex,
|
||||||
StudyInstanceUID
|
}
|
||||||
);
|
});
|
||||||
}, [displaySetInstanceUID, StudyInstanceUID]);
|
|
||||||
|
isVisible = _isDisplaySetReconstructable(
|
||||||
|
viewportSpecificData,
|
||||||
|
activeViewportIndex,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|||||||
@ -767,6 +767,16 @@ const isMultiFrame = instance => {
|
|||||||
return instance.getTagValue('NumberOfFrames') > 1;
|
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 makeDisplaySet = (series, instances) => {
|
||||||
const instance = instances[0];
|
const instance = instances[0];
|
||||||
const imageSet = new ImageSet(instances);
|
const imageSet = new ImageSet(instances);
|
||||||
@ -804,18 +814,22 @@ const makeDisplaySet = (series, instances) => {
|
|||||||
imageSet.getImage(0).getTagValue('InstanceNumber')
|
imageSet.getImage(0).getTagValue('InstanceNumber')
|
||||||
);
|
);
|
||||||
|
|
||||||
const isReconstructable = isDisplaySetReconstructable(instances);
|
const displayReconstructableInfo = isDisplaySetReconstructable(instances);
|
||||||
|
imageSet.isReconstructable = displayReconstructableInfo.value;
|
||||||
imageSet.isReconstructable = isReconstructable.value;
|
|
||||||
|
|
||||||
if (shallSort && imageSet.isReconstructable) {
|
if (shallSort && imageSet.isReconstructable) {
|
||||||
imageSet.sortByImagePositionPatient();
|
imageSet.sortByImagePositionPatient();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReconstructable.missingFrames) {
|
if (displayReconstructableInfo.missingFrames) {
|
||||||
// TODO -> This is currently unused, but may be used for reconstructing
|
// TODO -> This is currently unused, but may be used for reconstructing
|
||||||
// Volumes with gaps later on.
|
// 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;
|
return imageSet;
|
||||||
|
|||||||
11
platform/core/src/enums.js
Normal file
11
platform/core/src/enums.js
Normal file
@ -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};
|
||||||
@ -1,7 +1,12 @@
|
|||||||
|
import { ReconstructionIssues } from './../enums.js';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a series is reconstructable to a 3D volume.
|
* Checks if a series is reconstructable to a 3D volume.
|
||||||
*
|
*
|
||||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||||
|
*
|
||||||
|
* @returns {Object} reconstructable value, missingFrames and warningIssues.
|
||||||
*/
|
*/
|
||||||
export default function isDisplaySetReconstructable(instances) {
|
export default function isDisplaySetReconstructable(instances) {
|
||||||
if (!instances.length) {
|
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) {
|
function processMultiframe(instance) {
|
||||||
//TODO: deal with multriframe checks! return false for now as can't reconstruct.
|
const warningIssues = [ReconstructionIssues.MULTIFRAMES];
|
||||||
return { value: false };
|
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) {
|
function processSingleframe(instances) {
|
||||||
|
const n = instances.length;
|
||||||
const firstImage = instances[0].getData().metadata;
|
const firstImage = instances[0].getData().metadata;
|
||||||
const firstImageRows = firstImage.Rows;
|
const firstImageRows = firstImage.Rows;
|
||||||
const firstImageColumns = firstImage.Columns;
|
const firstImageColumns = firstImage.Columns;
|
||||||
@ -42,12 +63,13 @@ function processSingleframe(instances) {
|
|||||||
const firstImageOrientationPatient = firstImage.ImageOrientationPatient;
|
const firstImageOrientationPatient = firstImage.ImageOrientationPatient;
|
||||||
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
||||||
|
|
||||||
|
const warningIssues = [];
|
||||||
// Can't reconstruct if we:
|
// Can't reconstruct if we:
|
||||||
// -- Have a different dimensions within a displaySet.
|
// -- Have a different dimensions within a displaySet.
|
||||||
// -- Have a different number of components within a displaySet.
|
// -- Have a different number of components within a displaySet.
|
||||||
// -- Have different orientations within a displaySet.
|
// -- Have different orientations within a displaySet.
|
||||||
for (let i = 1; i < instances.length; i++) {
|
for (let ii = 1; ii < n; ++ii) {
|
||||||
const instance = instances[i].getData().metadata;
|
const instance = instances[ii].getData().metadata;
|
||||||
const {
|
const {
|
||||||
Rows,
|
Rows,
|
||||||
Columns,
|
Columns,
|
||||||
@ -55,13 +77,16 @@ function processSingleframe(instances) {
|
|||||||
ImageOrientationPatient,
|
ImageOrientationPatient,
|
||||||
} = instance;
|
} = instance;
|
||||||
|
|
||||||
if (
|
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
|
||||||
Rows !== firstImageRows ||
|
warningIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS);
|
||||||
Columns !== firstImageColumns ||
|
} else if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||||
SamplesPerPixel !== firstImageSamplesPerPixel ||
|
warningIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS);
|
||||||
!_isSameOrientation(ImageOrientationPatient, firstImageOrientationPatient)
|
} else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) {
|
||||||
) {
|
warningIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION);
|
||||||
return { value: false };
|
}
|
||||||
|
|
||||||
|
if (warningIssues.length !== 0) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,52 +95,101 @@ function processSingleframe(instances) {
|
|||||||
// Check if frame spacing is approximately equal within a spacingTolerance.
|
// Check if frame spacing is approximately equal within a spacingTolerance.
|
||||||
// If spacing is on a uniform grid but we are missing frames,
|
// If spacing is on a uniform grid but we are missing frames,
|
||||||
// Allow reconstruction, but pass back the number of missing frames.
|
// Allow reconstruction, but pass back the number of missing frames.
|
||||||
if (instances.length > 2) {
|
if (n > 2) {
|
||||||
const lastIpp = instances[instances.length - 1].getData().metadata
|
const lastIpp = instances[n - 1].getData().metadata
|
||||||
.ImagePositionPatient;
|
.ImagePositionPatient;
|
||||||
|
|
||||||
// We can't reconstruct if we are missing ImagePositionPatient values
|
// We can't reconstruct if we are missing ImagePositionPatient values
|
||||||
if (!firstImagePositionPatient || !lastIpp) {
|
if (firstImagePositionPatient && lastIpp) {
|
||||||
return { value: false };
|
const averageSpacingBetweenFrames =
|
||||||
}
|
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||||
|
(n - 1);
|
||||||
|
|
||||||
const averageSpacingBetweenFrames =
|
let previousImagePositionPatient = firstImagePositionPatient;
|
||||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
|
||||||
(instances.length - 1);
|
|
||||||
|
|
||||||
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 spacingBetweenFrames = _getPerpendicularDistance(
|
||||||
const instance = instances[i].getData().metadata;
|
ImagePositionPatient,
|
||||||
const { ImagePositionPatient } = instance;
|
previousImagePositionPatient
|
||||||
|
);
|
||||||
|
const spacingIssue = _getSpacingIssue(
|
||||||
|
spacingBetweenFrames,
|
||||||
|
averageSpacingBetweenFrames
|
||||||
|
);
|
||||||
|
|
||||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
if (spacingIssue) {
|
||||||
ImagePositionPatient,
|
const issue = spacingIssue.issue;
|
||||||
previousImagePositionPatient
|
|
||||||
);
|
|
||||||
const spacingIssue = _getSpacingIssue(
|
|
||||||
spacingBetweenFrames,
|
|
||||||
averageSpacingBetweenFrames
|
|
||||||
);
|
|
||||||
|
|
||||||
if (spacingIssue) {
|
if (issue === ReconstructionIssues.MISSING_FRAMES) {
|
||||||
const issue = spacingIssue.issue;
|
missingFrames += spacingIssue.missingFrames;
|
||||||
|
} else if (issue === ReconstructionIssues.IRREGULAR_SPACING) {
|
||||||
if (issue === reconstructionIssues.MISSING_FRAMES) {
|
warningIssues.push(issue);
|
||||||
missingFrames += spacingIssue.missingFrames;
|
break;
|
||||||
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
|
}
|
||||||
return { value: false };
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
if (iop1 === undefined || !iop2 === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -156,12 +230,12 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
|||||||
|
|
||||||
if (errorForEachSpacing < spacingTolerance * averageSpacing) {
|
if (errorForEachSpacing < spacingTolerance * averageSpacing) {
|
||||||
return {
|
return {
|
||||||
issue: reconstructionIssues.MISSING_FRAMES,
|
issue: ReconstructionIssues.MISSING_FRAMES,
|
||||||
missingFrames: numberOfSpacings - 1,
|
missingFrames: numberOfSpacings - 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { issue: reconstructionIssues.IRREGULAR_SPACING };
|
return { issue: ReconstructionIssues.IRREGULAR_SPACING };
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getPerpendicularDistance(a, b) {
|
function _getPerpendicularDistance(a, b) {
|
||||||
@ -173,7 +247,3 @@ function _getPerpendicularDistance(a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
||||||
const reconstructionIssues = {
|
|
||||||
MISSING_FRAMES: 'missingframes',
|
|
||||||
IRREGULAR_SPACING: 'irregularspacing',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -28,6 +28,7 @@ function StudyBrowser(props) {
|
|||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
stackPercentComplete,
|
stackPercentComplete,
|
||||||
|
hasWarnings,
|
||||||
} = thumb;
|
} = thumb;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -50,6 +51,7 @@ function StudyBrowser(props) {
|
|||||||
numImageFrames={numImageFrames}
|
numImageFrames={numImageFrames}
|
||||||
SeriesDescription={SeriesDescription}
|
SeriesDescription={SeriesDescription}
|
||||||
SeriesNumber={SeriesNumber}
|
SeriesNumber={SeriesNumber}
|
||||||
|
hasWarnings={hasWarnings}
|
||||||
stackPercentComplete={stackPercentComplete}
|
stackPercentComplete={stackPercentComplete}
|
||||||
// Events
|
// Events
|
||||||
onClick={onThumbnailClick.bind(
|
onClick={onThumbnailClick.bind(
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useDrag } from 'react-dnd';
|
import { useDrag } from 'react-dnd';
|
||||||
import ImageThumbnail from './ImageThumbnail';
|
import ImageThumbnail from './ImageThumbnail';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { Icon } from './../../elements/Icon';
|
||||||
|
import { Tooltip } from './../tooltip';
|
||||||
|
import { OverlayTrigger } from './../overlayTrigger';
|
||||||
|
|
||||||
import './Thumbnail.styl';
|
import './Thumbnail.styl';
|
||||||
|
|
||||||
@ -11,7 +14,22 @@ function ThumbnailFooter({
|
|||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
InstanceNumber,
|
InstanceNumber,
|
||||||
numImageFrames,
|
numImageFrames,
|
||||||
|
hasWarnings
|
||||||
}) {
|
}) {
|
||||||
|
const [warningList, warningListSet] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unmounted = false
|
||||||
|
hasWarnings.then(response => {
|
||||||
|
if (!unmounted) {
|
||||||
|
warningListSet(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
unmounted = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const infoOnly = !SeriesDescription;
|
const infoOnly = !SeriesDescription;
|
||||||
|
|
||||||
const getInfo = (value, icon, className = '') => {
|
const getInfo = (value, icon, className = '') => {
|
||||||
@ -22,28 +40,73 @@ function ThumbnailFooter({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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, warningList) => {
|
||||||
|
return(
|
||||||
|
<React.Fragment>
|
||||||
|
{warningList.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(warningList)}</div>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={classNames('warning')}>
|
||||||
|
<span className="warning-icon">
|
||||||
|
<Icon name="exclamation-triangle" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
|
) : (
|
||||||
|
<React.Fragment></React.Fragment>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
const getSeriesInformation = (
|
const getSeriesInformation = (
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
InstanceNumber,
|
InstanceNumber,
|
||||||
numImageFrames
|
numImageFrames,
|
||||||
|
warningList
|
||||||
) => {
|
) => {
|
||||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const seriesInformation =
|
||||||
return (
|
|
||||||
<div className="series-information">
|
<div className="series-information">
|
||||||
{getInfo(SeriesNumber, 'S:')}
|
{getInfo(SeriesNumber, 'S:')}
|
||||||
{getInfo(InstanceNumber, 'I:')}
|
{getInfo(InstanceNumber, 'I:')}
|
||||||
{getInfo(numImageFrames, '', 'image-frames')}
|
{getInfo(numImageFrames, '', 'image-frames')}
|
||||||
|
{getWarningInfo(SeriesNumber, warningList)}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
return (seriesInformation);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
<div className={classNames('series-details', { 'info-only': infoOnly })}>
|
||||||
<div className="series-description">{SeriesDescription}</div>
|
<div className="series-description">{SeriesDescription}</div>
|
||||||
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames)}
|
{getSeriesInformation(SeriesNumber, InstanceNumber, numImageFrames, warningList)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -60,6 +123,7 @@ function Thumbnail(props) {
|
|||||||
numImageFrames,
|
numImageFrames,
|
||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
hasWarnings,
|
||||||
stackPercentComplete,
|
stackPercentComplete,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
onClick,
|
onClick,
|
||||||
@ -126,13 +190,14 @@ Thumbnail.propTypes = {
|
|||||||
stackPercentComplete: PropTypes.number,
|
stackPercentComplete: PropTypes.number,
|
||||||
/**
|
/**
|
||||||
altImageText will be used when no imageId or imageSrc is provided.
|
altImageText will be used when no imageId or imageSrc is provided.
|
||||||
It will be displayed inside the <div>. This is useful when it is difficult
|
It will be displayed inside the <div>. This is useful when it is difficult
|
||||||
to make a preview for a type of DICOM series (e.g. DICOM-SR)
|
to make a preview for a type of DICOM series (e.g. DICOM-SR)
|
||||||
*/
|
*/
|
||||||
altImageText: PropTypes.string,
|
altImageText: PropTypes.string,
|
||||||
SeriesDescription: PropTypes.string,
|
SeriesDescription: PropTypes.string,
|
||||||
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
hasWarnings: PropTypes.instanceOf(Promise),
|
||||||
numImageFrames: PropTypes.number,
|
numImageFrames: PropTypes.number,
|
||||||
onDoubleClick: PropTypes.func,
|
onDoubleClick: PropTypes.func,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
|||||||
@ -78,6 +78,17 @@
|
|||||||
height: 11px
|
height: 11px
|
||||||
width: 11px
|
width: 11px
|
||||||
|
|
||||||
|
.warning
|
||||||
|
margin: auto 0;
|
||||||
|
opacity: 1
|
||||||
|
color: #e29e4a;
|
||||||
|
|
||||||
|
svg
|
||||||
|
width: 16px;
|
||||||
|
height: 14px;
|
||||||
|
pointer-events: inherit;
|
||||||
|
|
||||||
|
|
||||||
.value
|
.value
|
||||||
color: var(--text-secondary-color);
|
color: var(--text-secondary-color);
|
||||||
display: inline-block
|
display: inline-block
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import ConnectedViewerMain from './ConnectedViewerMain.js';
|
|||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
||||||
import { extensionManager } from './../App.js';
|
import { extensionManager } from './../App.js';
|
||||||
|
import { ReconstructionIssues } from './../../../core/src/enums.js';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
||||||
@ -367,6 +368,62 @@ class Viewer extends Component {
|
|||||||
|
|
||||||
export default withDialog(Viewer);
|
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
|
* What types are these? Why do we have "mapping" dropped in here instead of in
|
||||||
* a mapping layer?
|
* a mapping layer?
|
||||||
@ -406,6 +463,8 @@ const _mapStudiesToThumbnails = function(studies) {
|
|||||||
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasWarnings = _checkForSeriesInconsistencesWarnings(displaySet)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageId,
|
imageId,
|
||||||
altImageText,
|
altImageText,
|
||||||
@ -414,6 +473,7 @@ const _mapStudiesToThumbnails = function(studies) {
|
|||||||
InstanceNumber,
|
InstanceNumber,
|
||||||
numImageFrames,
|
numImageFrames,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
hasWarnings,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user