diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js
index 256b1d0b8..7d86dc5b4 100644
--- a/platform/core/src/classes/metadata/StudyMetadata.js
+++ b/platform/core/src/classes/metadata/StudyMetadata.js
@@ -9,6 +9,7 @@ import { SeriesMetadata } from './SeriesMetadata';
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from '../../utils/isImage';
+import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata {
@@ -620,6 +621,16 @@ const makeDisplaySet = (series, instances) => {
imageSet.getImage(0).getRawValue('x00200013')
);
+ const isReconstructable = isDisplaySetReconstructable(series, instances);
+
+ imageSet.isReconstructable = isReconstructable.value;
+
+ if (isReconstructable.missingFrames) {
+ // TODO -> This is currently unused, but may be used for reconstructing
+ // Volumes with gaps later on.
+ imageSet.missingFrames = isReconstructable.missingFrames;
+ }
+
return imageSet;
};
diff --git a/platform/core/src/utils/isDisplaySetReconstructable.js b/platform/core/src/utils/isDisplaySetReconstructable.js
new file mode 100644
index 000000000..950320540
--- /dev/null
+++ b/platform/core/src/utils/isDisplaySetReconstructable.js
@@ -0,0 +1,157 @@
+/**
+ * Checks if a series is reconstructable to a 3D volume.
+ *
+ * @param {Object} series The `OHIFSeriesMetadata` object.
+ * @param {Object[]} instances The `OHIFInstanceMetadata` object
+ */
+export default function isDisplaySetReconstructable(series, instances) {
+ // Can't reconstruct if we only have one image.
+
+ const modality = series._data.modality; // TODO -> Is there a better way to get this?
+ const isMultiframe = instances[0].getRawValue('x00280008') > 1;
+
+ if (!constructableModalities.includes(modality)) {
+ return { value: false };
+ }
+
+ if (!isMultiframe && instances.length === 1) {
+ return { values: false };
+ }
+
+ if (isMultiframe) {
+ return processMultiframe(instances[0]);
+ } else {
+ return processSingleframe(instances);
+ }
+}
+
+function processMultiframe(instance) {
+ //TODO: deal with multriframe checks! return true for now.
+ return { value: true };
+}
+
+function processSingleframe(instances) {
+ const firstImage = instances[0];
+ const firstImageRows = firstImage.getTagValue('x00280010');
+ const firstImageColumns = firstImage.getTagValue('x00280011');
+ const firstImageSamplesPerPixel = firstImage.getTagValue('x00280002');
+ // Note: No need to unpack iop, can compare string form.
+ const firstImageOrientationPatient = firstImage.getTagValue('x00200037');
+
+ // 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];
+ const rows = instance.getTagValue('x00280010');
+ const columns = instance.getTagValue('x00280011');
+ const samplesPerPixel = instance.getTagValue('x00280002');
+ const imageOrientationPatient = instance.getTagValue('x00200037');
+
+ if (
+ rows !== firstImageRows ||
+ columns !== firstImageColumns ||
+ samplesPerPixel !== firstImageSamplesPerPixel ||
+ imageOrientationPatient !== firstImageOrientationPatient
+ ) {
+ return { value: false };
+ }
+ }
+
+ let missingFrames = 0;
+
+ // Check if frame spacing is approximately equal within a tolerance.
+ // 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 firstIpp = _getImagePositionPatient(firstImage);
+ const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
+ const averageSpacingBetweenFrames =
+ _getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
+
+ let previousIpp = firstIpp;
+
+ for (let i = 1; i < instances.length; i++) {
+ const instance = instances[i];
+ const ipp = _getImagePositionPatient(instance);
+
+ const spacingBetweenFrames = _getPerpendicularDistance(ipp, previousIpp);
+ const spacingIssue = _getSpacingIssue(
+ spacingBetweenFrames,
+ averageSpacingBetweenFrames
+ );
+
+ if (spacingIssue) {
+ const issue = spacingIssue.issue;
+
+ if (issue === reconstructionIssues.MISSING_FRAMES) {
+ missingFrames += spacingIssue.missingFrames;
+ } else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
+ return { value: false };
+ }
+ }
+
+ previousIpp = ipp;
+ }
+ }
+
+ return { value: true, missingFrames };
+}
+
+// TODO: Is 10% a reasonable tolerance for spacing?
+const tolerance = 0.1;
+
+/**
+ * Checks for spacing issues.
+ *
+ * @param {number} spacing The spacing between two frames.
+ * @param {number} averageSpacing The average spacing between all frames.
+ *
+ * @returns {Object} An object containing the issue and extra information if necessary.
+ */
+function _getSpacingIssue(spacing, averageSpacing) {
+ const equalWithinTolerance =
+ Math.abs(spacing - averageSpacing) < averageSpacing * tolerance;
+
+ if (equalWithinTolerance) {
+ return;
+ }
+
+ const multipleOfAverageSpacing = spacing / averageSpacing;
+
+ const numberOfSpacings = Math.round(multipleOfAverageSpacing);
+
+ const errorForEachSpacing =
+ Math.abs(spacing - numberOfSpacings * averageSpacing) / numberOfSpacings;
+
+ if (errorForEachSpacing < tolerance * averageSpacing) {
+ return {
+ issue: reconstructionIssues.MISSING_FRAMES,
+ missingFrames: numberOfSpacings - 1,
+ };
+ }
+
+ return { issue: reconstructionIssues.IRREGULAR_SPACING };
+}
+
+function _getImagePositionPatient(instance) {
+ return instance
+ .getTagValue('x00200032')
+ .split('\\')
+ .map(element => Number(element));
+}
+
+function _getPerpendicularDistance(a, b) {
+ return Math.sqrt(
+ Math.pow(a[0] - b[0], 2) +
+ Math.pow(a[1] - b[1], 2) +
+ Math.pow(a[2] - b[2], 2)
+ );
+}
+
+const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
+const reconstructionIssues = {
+ MISSING_FRAMES: 'missingframes',
+ IRREGULAR_SPACING: 'irregularspacing',
+};
diff --git a/platform/viewer/cypress/integration/common/OHIFCornerstoneToolbar.spec.js b/platform/viewer/cypress/integration/common/OHIFCornerstoneToolbar.spec.js
index d4a15720c..0ce8c3560 100644
--- a/platform/viewer/cypress/integration/common/OHIFCornerstoneToolbar.spec.js
+++ b/platform/viewer/cypress/integration/common/OHIFCornerstoneToolbar.spec.js
@@ -42,9 +42,6 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@moreBtn')
.should('be.visible')
.contains('More');
- cy.get('@twodmprBtn')
- .should('be.visible')
- .contains('2D MPR');
cy.get('@layoutBtn')
.should('be.visible')
.contains('Layout');
diff --git a/platform/viewer/cypress/support/aliases.js b/platform/viewer/cypress/support/aliases.js
index b914c506c..37fa12ec2 100644
--- a/platform/viewer/cypress/support/aliases.js
+++ b/platform/viewer/cypress/support/aliases.js
@@ -10,7 +10,6 @@ export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(9)').as('resetBtn');
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
- cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
}
diff --git a/platform/viewer/src/connectedComponents/ConnectedPluginSwitch.js b/platform/viewer/src/connectedComponents/ConnectedPluginSwitch.js
index 0e46e77f9..130bd0eba 100644
--- a/platform/viewer/src/connectedComponents/ConnectedPluginSwitch.js
+++ b/platform/viewer/src/connectedComponents/ConnectedPluginSwitch.js
@@ -6,10 +6,8 @@ import { connect } from 'react-redux';
const { setLayout } = OHIF.redux.actions;
-const ConnectedPluginSwitch = (props) => {
- return (
-