feat: cornerstone3D stack and volume viewports (#2787)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: cs3d tools and toolGroups (#20)

* add more tools to work with cs3d mode

* fix hotkeys

* add stack manager usage for stach viewports

* add image scrollbar

* wip viewport overlay

* fix toAnnotation schema for tools

* fix the unnecessary size change that triggered resize

* hanging protocol improvement to allow unmatched errors

* study description matching for hanging protocol

* fix the displaysetOptions to work

* fix handle the active tool when a new viewport is added

* fix separate toolGroups for mode

* apply review comments

* apply review comments

* yarn lock

* feat: overlay component (#21)

* fix default displayset options

* add viewport overlay

* apply review comments

* feat: loading and orientation indicators (#22)

* add loading indicator

* add orientation marker initial work

* apply review comments

* fix: orientation markers (#23)

* finished the orientation markers

* fix various broken cypress tests

* apply review comments

* update yarn lock

* feat: re-working measurement tracking mode with cornerstone3d (#2805)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: Add Measurement tracking mode with cs3D (#2789)

* feat: first render for cornerstone3d tracked viewport

* make tool active work

* wip for SR extension

* renamed dicom sr to cornerstone dicom sr

* remove cornerstone from dicom pdf and video

* move dicom sr logic to sr extension

* feat: Add hydration for the length tool

* fix SR display tool for length using cs3d

* fix default config

* fix: various bugs with sr viewport and tracking

* fix promptying to continue tracking for when SR is created

* feat: add keep trackign of unique identifiers

* fix hydration for same imageIds

* feat: Add SR toolGroup creation on modeEnter

* feat: remove the need for separate mapper for SR hydration

* add SR display for ellipse

* handle hydration of elliptical ROI tool

* remove cornerstone extension

* add arrow mapping

* feat: Add ArrowAnnotate SR display and hydration

* apply review comments

* apply review comments

* move viewport labels to the viewportData

* apply review comments

* fix: integration cypress tests with cornerstone3D and add CINE tool (#2795)

* fix: integration cypress tests with cornerstone3D

* revert to addOrUpdate as it makes more sense

* fix local drag and drop

* fix tests

* move dicomLoaderService to cornerstone extension

* fix various import bugs

* fix bug for local PT series

* fix various unit tests

* bump cs3d versions

* add angle and magnify tool

* bump dependencies to avoid broken peerDeps

* feat: add initial work for capture using cs3d

* feat: show annotations on the image capture

* feat: add svg layer export

* feat: Add CINE Tool

* feat: remove unnecessary viewport rendering for cine state changes

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>

* docs: modify and improve documentation (#2800)

* cleanup docs versionings

* feat: Add all version explanations

* version docs for 3.0

* wip for changing docs

* wip for updated docs

* add utility module documentation

* fix demo with nohoisting of history

* add slides and video to resources

* apply review comments

* fix: drag and drop SR into SR viewport (#2803)

* update yarn lock

Co-authored-by: Erik <erik.sweed@gmail.com>

* update pathnames to match v3-stable

* update the e2e pathname

* fix: various bugs with regard to tracking workflow (#2811)

* fix: various issues with measurement panel

* fix: update default tool style for annotations

* fix: annotatoin label getting removed

* feat: Add backward compatibility for SR hydration with legacy cornerstone

* fix: cursors and ellipse ROI max style

* fix: ArrowAnnotate SRDisplay

* apply review comments

* bump package versions

* fix: bugin rehydration of SR

* fix: e2e tests

* fix active viewport thickness and arrowTool ui

* add readme for measurement tracking

* use uploaded image for readme

* add back images

* try to fix e2e test

* fix: window level presets hotkeys

* Update README.md

* update yarn lock

* feat: volume api and TMTV mode (#2817)

* feat: volumeAPI and TMTV mode

* feat: use cs3d cache service to obtain viewportData

* wip for volume api

* wip: fusion viewport

* feat: add blend mode option

* wip for image scrollbar

* fix drag and drop thumbnail

* wip for image scrollbar for voluems

* fix: element mismatch bug for scroll

* feat: Add image scrollbar to volumes

* feat: add syncGroups to volume api

* feat: add tmtv mode initial setup

* feat: add initial image options for the stack viewports

* feat: Add custom load strategy for volume viewports via HP

* apply review comments

fix: Jump presets cs3d (#2812)

* feat: Add JumpPreset to OHIF for Cornerstone3D

* fix: accessing viewport service from servicesManager

feat: volume API and TMTV mode (#2814)

* fix: do not display overlays on mip viewports

* feat: add optional disableCommands for toggle buttons

* fix: toggleCrossharis

* feat: config the crosshairs

* feat: add PetSUV Panel for changing metadata

* feat: initial work for the rectangleROIThreshold panel

* wip: measurement service

* roi threshold working

* feat: Add displayText to segmentations

* feat: add remove segmentation

* feat: add csv export

* feat: add RT export for annotations in tmtv mode

* fix: fusion to use pt in tmtv mode and measuremet mappings

* fix: various bugs

* apply review comments

* apply review comments

* feat: add fusion color maps

* add readme to tmtv mode

* Update README.md

* fix: try to fix build

* fix unit tests

* Update README.md

* feat: add about to the panel

* fix: changing strategy in roi panel

* apply review comments

* update package versions

* wip for stackPrefetch

* fix: cornerstone3d hydration and renaming (#2818)

* update readme

* renamed cornerstone extension

* wip for renaming cornerstone3D variables

* wip for renaming cornerstone3D variables

* wip for fixing bugs for SR viewport

* fix: jumpToMeasurement and initial label after hydration

* fix: fileName capitalization

* feat: use the new prefetch stack in the cs3d (#2820)

* feat: use the new prefetch stack in the cs3d

* use viewport scroll api for stack viewport

* fix cine stop when scrollbar changes

* feat: use new prefetch events

* fix loading state to not show repeatedly

* fix: various bugs for tmtv mode thresholding and new icons (#2823)

* feat: make tmtv mode available in worklist

* feat: add new icons for tmtv mode

* feat: add fusion color icon

* fix: parallel scale calculation

* fix: bump Cornerstone to get large image support working

* fix: Fix issues with CPU viewport flipping, including config files

* fix: bump cornerstone to fix magnify tool

* fix: Bump Cornerstone version to fix resetCamera issue in StackViewport

* ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys

* fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors

* feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838)

Also works with the compile time add that the existing cli uses, so that both build types work.
Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes.

* bump: dependency versions to fix hydration bugs (#2848)

* bump: dcmjs version to fix hydration bugs

* try to fix tests

* bump dependency versions

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
authored and GitHub committed 2022-07-27 12:39:04 -04:00
1 parent 86dcaf6293
commit bd1c38c00c
668 files changed
+41669 -13410

No files matched your search

@@ -14,7 +14,6 @@ module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
+5 -5
View File
@@ -6,7 +6,7 @@
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"module": "src/index.tsx",
"publishConfig": {
"access": "public"
},
@@ -32,13 +32,13 @@
"peerDependencies": {
"@ohif/core": "^3.0.0",
"classnames": "^2.2.6",
"cornerstone-core": "^2.6.0",
"cornerstone-tools": "6.0.2",
"dcmjs": "0.16.1",
"@cornerstonejs/core": "^0.13.11",
"@cornerstonejs/tools": "^0.20.15",
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
"dcmjs": "^0.24.5",
"prop-types": "^15.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-cornerstone-viewport": "^4.1.2",
"webpack": "^5.50.0",
"webpack-merge": "^5.7.3",
"@ohif/ui": "^2.0.0"
@@ -1,5 +1,5 @@
import React from 'react';
import { DICOMSR, DicomMetadataStore } from '@ohif/core';
import { DicomMetadataStore } from '@ohif/core';
/**
*
@@ -11,6 +11,7 @@ import { DICOMSR, DicomMetadataStore } from '@ohif/core';
*/
async function createReportAsync(
servicesManager,
commandsManager,
dataSource,
measurements,
options
@@ -29,11 +30,15 @@ async function createReportAsync(
});
try {
const naturalizedReport = await DICOMSR.storeMeasurements(
measurements,
dataSource,
['ArrowAnnotate'],
options
const naturalizedReport = await commandsManager.runCommand(
'storeMeasurements',
{
measurementData: measurements,
dataSource,
additionalFindingTypes: ['ArrowAnnotate'],
options,
},
'CORNERSTONE_STRUCTURED_REPORT'
);
// The "Mode" route listens for DicomMetadataStore changes
@@ -1,165 +0,0 @@
import { DicomMetadataStore } from '@ohif/core';
export default function downloadCSVReport(
measurementData,
StudyInstanceUID,
referencedSeriesInstanceUIDs,
MeasurementService
) {
const { VALUE_TYPES } = MeasurementService;
if (measurementData.length === 0) {
// Prevent download of report with no measurements.
return;
}
const seriesMetadataMap = {};
referencedSeriesInstanceUIDs.forEach(SeriesInstanceUID => {
seriesMetadataMap[SeriesInstanceUID] = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
});
const rows = [
[
'Patient ID',
'Patient Name',
'StudyInstanceUID',
'SeriesInstanceUID',
'SOPInstanceUID',
'Label',
'Long Axis (mm)',
'Short Axis (mm)',
'Length (mm)',
'Area (mmˆ2)',
'Mean',
'StDev',
'Units',
],
];
measurementData.forEach(measurement => {
switch (measurement.type) {
case VALUE_TYPES.BIDIRECTIONAL:
rows.push(_getBidirectionalRow(measurement, seriesMetadataMap));
break;
case VALUE_TYPES.ELLIPSE:
rows.push(_getEllipseRow(measurement, seriesMetadataMap));
break;
case VALUE_TYPES.POINT:
rows.push(_getAdditionalFindingRow(measurement, seriesMetadataMap));
break;
case VALUE_TYPES.POLYLINE:
rows.push(_getLengthRow(measurement, seriesMetadataMap));
}
});
let csvContent =
'data:text/csv;charset=utf-8,' + rows.map(row => row.join(',')).join('\n');
_createAndDownloadFile(csvContent);
}
function _getBidirectionalRow(measurement, seriesMetadataMap) {
const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap);
return [
...commonRowItems,
measurement.longestDiameter, // Long Axis (mm)
measurement.shortestDiameter, // Short Axis (mm)
];
}
function _getCommonRowItems(measurement, seriesMetadataMap) {
const seriesMetadata = seriesMetadataMap[measurement.referenceSeriesUID];
const firstInstance = seriesMetadata.instances[0];
const row = [
firstInstance.PatientID, // Patient ID
firstInstance.PatientName.Alphabetic, // PatientName
measurement.referenceStudyUID, // StudyInstanceUID
measurement.referenceSeriesUID, // SeriesInstanceUID
measurement.SOPInstanceUID, // SOPInstanceUID
measurement.label || '', // Label
];
return row;
}
function _getAdditionalFindingRow(measurement, seriesMetadataMap) {
const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap);
return [...commonRowItems];
}
function _getEllipseRow(measurement, seriesMetadataMap) {
const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap);
const blankColumns = ['', '', ''];
const { area, mean, stdDev, unit } = _getEllipseStatsWithUnits(
measurement,
seriesMetadataMap
);
return [
...commonRowItems,
...blankColumns,
area, // Area (mmˆ2)
mean, // Mean
stdDev, // StDev
unit,
];
}
function _getEllipseStatsWithUnits(measurement, seriesMetadataMap) {
const seriesMetadata = seriesMetadataMap[measurement.referenceSeriesUID];
const firstInstance = seriesMetadata.instances[0];
const modality = firstInstance.Modality;
switch (modality) {
case 'CT':
return {
mean: `${measurement.mean.toFixed(1)}`,
stdDev: `${measurement.stdDev.toFixed(1)}`,
area: measurement.area.toFixed(1),
unit: 'HU',
};
case 'PT':
if (measurement.meanSUV) {
return {
mean: `${measurement.meanSUV.toFixed(1)}`,
stdDev: `${measurement.stdDevSUV.toFixed(1)}`,
area: measurement.area.toFixed(1),
unit: 'SUV',
};
}
// If no SUV calculation then fallback to default.
default:
return {
mean: `${measurement.mean.toFixed(1)}`,
stdDev: `${measurement.stdDev.toFixed(1)}`,
area: measurement.area.toFixed(1),
unit: 'Pixel Values',
};
}
}
function _getLengthRow(measurement, seriesMetadataMap) {
const commonRowItems = _getCommonRowItems(measurement, seriesMetadataMap);
const blankColumns = ['', ''];
return [...commonRowItems, ...blankColumns, measurement.length.toFixed(1)];
}
function _createAndDownloadFile(csvContent) {
const encodedUri = encodeURI(csvContent);
const link = document.createElement('a');
link.setAttribute('href', encodedUri);
link.setAttribute('download', 'MeasurementReport.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
@@ -1,36 +0,0 @@
import csTools from 'cornerstone-tools';
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
csTools.OverlayTool,
],
};
export default function getTools() {
const tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
return tools;
}
export { toolsGroupedByType };
@@ -1,21 +0,0 @@
import csTools from 'cornerstone-tools';
export default function _setActiveAndPassiveToolsForElement(element, tools) {
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
tools.forEach(tool => {
if (tool.prototype instanceof BaseAnnotationTool) {
// BaseAnnotationTool would likely come from csTools lib exports
const toolName = new tool().name;
csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class
}
});
csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 });
csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 });
csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 });
csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required
csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {});
csTools.setToolEnabledForElement(element, 'Overlay', {});
}
@@ -1,39 +0,0 @@
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
export default function setCornerstoneMeasurementActive(measurement) {
const { id } = measurement;
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
Object.keys(toolState).forEach(imageId => {
const imageIdSpecificToolState = toolState[imageId];
Object.keys(imageIdSpecificToolState).forEach(toolType => {
const toolSpecificToolState = imageIdSpecificToolState[toolType];
const toolSpecificToolData = toolSpecificToolState.data;
if (toolSpecificToolData && toolSpecificToolData.length) {
toolSpecificToolData.forEach(data => {
data.active = data.id === id ? true : false;
});
}
});
});
const enabledElements = cornerstoneTools.store.state.enabledElements;
enabledElements.forEach(element => {
try {
cornerstone.updateImage(element);
} catch (ex) {
// https://github.com/cornerstonejs/cornerstone/blob/master/src/updateImage.js#L16
// This fails if enabledElement.image is undefined and we have no layers
// Instead of throwing, it should _probably_ do nothing.
// We'll just swallow the exception
}
});
}
@@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect } from 'react';
import React, { useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
@@ -18,14 +18,14 @@ TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
const SR_SOPCLASSHANDLERID =
'@ohif/extension-dicom-sr.sopClassHandlerModule.dicom-sr';
'@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr';
/**
*
* @param {*} param0
*/
function TrackedMeasurementsContextProvider(
{ servicesManager, extensionManager }, // Bound by consumer
{ servicesManager, commandsManager, extensionManager }, // Bound by consumer
{ children } // Component props
) {
const [viewportGrid, viewportGridService] = useViewportGrid();
@@ -34,10 +34,8 @@ function TrackedMeasurementsContextProvider(
const machineOptions = Object.assign({}, defaultOptions);
machineOptions.actions = Object.assign({}, machineOptions.actions, {
jumpToFirstMeasurementInActiveViewport: (ctx, evt) => {
const {
DisplaySetService,
MeasurementService,
} = servicesManager.services;
const { MeasurementService } = servicesManager.services;
const { trackedStudy, trackedSeries } = ctx;
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
@@ -46,11 +44,11 @@ function TrackedMeasurementsContextProvider(
trackedSeries.includes(m.referenceSeriesUID)
);
const id = trackedMeasurements[0].id;
const uid = trackedMeasurements[0].uid;
MeasurementService.jumpToMeasurement(
viewportGrid.activeViewportIndex,
id
uid
);
},
showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
@@ -58,9 +56,9 @@ function TrackedMeasurementsContextProvider(
const StructuredReportDisplaySetInstanceUID =
evt.data.createdDisplaySetInstanceUIDs[0].displaySetInstanceUID;
viewportGridService.setDisplaysetForViewport({
viewportGridService.setDisplaySetsForViewport({
viewportIndex: evt.data.viewportIndex,
displaySetInstanceUID: StructuredReportDisplaySetInstanceUID,
displaySetInstanceUIDs: [StructuredReportDisplaySetInstanceUID],
});
}
},
@@ -73,16 +71,16 @@ function TrackedMeasurementsContextProvider(
const measurementIds = filteredMeasurements.map(fm => fm.id);
for (let i = 0; i < measurementIds.length; i++) {
MeasurementService.remove(measurementIds[i], 'app-source');
MeasurementService.remove(measurementIds[i]);
}
},
clearAllMeasurements: (ctx, evt) => {
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const measurementIds = measurements.map(fm => fm.id);
const measurementIds = measurements.map(fm => fm.uid);
for (let i = 0; i < measurementIds.length; i++) {
MeasurementService.remove(measurementIds[i], 'app-source');
MeasurementService.remove(measurementIds[i]);
}
},
});
@@ -101,6 +99,7 @@ function TrackedMeasurementsContextProvider(
}),
promptSaveReport: promptSaveReport.bind(null, {
servicesManager,
commandsManager,
extensionManager,
}),
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
@@ -134,13 +133,15 @@ function TrackedMeasurementsContextProvider(
if (viewports.length > 0) {
const activeViewport = viewports[activeViewportIndex];
if (!activeViewport || !activeViewport.displaySetInstanceUID) {
if (!activeViewport || !activeViewport?.displaySetInstanceUIDs?.length) {
return;
}
// Todo: Getting the first displaySetInstanceUID is wrong, but we don't have
// tracking fusion viewports yet. This should change when we do.
const { DisplaySetService } = servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUID
activeViewport.displaySetInstanceUIDs[0]
);
// If this is an SR produced by our SR SOPClassHandler,
@@ -197,6 +198,7 @@ function TrackedMeasurementsContextProvider(
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
servicesManager: PropTypes.object.isRequired,
commandsManager: PropTypes.object.isRequired,
extensionManager: PropTypes.object.isRequired,
};
@@ -1,12 +1,15 @@
import cornerstoneTools from 'cornerstone-tools';
import { utilities, metaData } from '@cornerstonejs/core';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
import getCornerstoneToolStateToMeasurementSchema from './getCornerstoneToolStateToMeasurementSchema';
import { adapters } from 'dcmjs';
const { guid } = OHIF.utils;
const globalImageIdSpecificToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager;
const { MeasurementReport, CORNERSTONE_3D_TAG } = adapters.Cornerstone3D;
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
/**
*
@@ -24,8 +27,8 @@ export default function _hydrateStructuredReport(
// TODO -> We should define a strict versioning somewhere.
const mappings = MeasurementService.getSourceMappings(
'CornerstoneTools',
'4'
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
if (!mappings || !mappings.length) {
@@ -40,29 +43,38 @@ export default function _hydrateStructuredReport(
displaySet.SOPInstanceUID
);
const { MeasurementReport } = adapters.Cornerstone;
const sopInstanceUIDToImageId = {};
let imageIdsForToolState = [];
displaySet.measurements.forEach(measurement => {
const { ReferencedSOPInstanceUID, imageId } = measurement;
imageIdsForToolState.push(imageId);
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
}
});
const datasetToUse = _mapLegacyDataSet(instance);
// Use dcmjs to generate toolState.
const storedMeasurementByToolType = MeasurementReport.generateToolState(
instance
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
datasetToUse,
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
// for the imageToWorld transformation. The following assumes that the order
// that measurements were added to the display set are the same order as
// the measurementGroups in the instance.
sopInstanceUIDToImageId,
utilities.imageToWorldCoords,
metaData
);
// Filter what is found by DICOM SR to measurements we support.
const mappingDefinitions = mappings.map(m => m.definition);
const mappingDefinitions = mappings.map(m => m.annotationType);
const hydratableMeasurementsInSR = {};
Object.keys(storedMeasurementByToolType).forEach(key => {
Object.keys(storedMeasurementByAnnotationType).forEach(key => {
if (mappingDefinitions.includes(key)) {
hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key];
hydratableMeasurementsInSR[key] = storedMeasurementByAnnotationType[key];
}
});
@@ -70,12 +82,13 @@ export default function _hydrateStructuredReport(
const imageIds = [];
// TODO: notification if no hydratable?
Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
const toolDataForToolType = hydratableMeasurementsInSR[toolType];
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType =
hydratableMeasurementsInSR[annotationType];
toolDataForToolType.forEach(data => {
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
const imageId = sopInstanceUIDToImageId[toolData.sopInstanceUid];
if (!imageIds.includes(imageId)) {
imageIds.push(imageId);
@@ -88,7 +101,7 @@ export default function _hydrateStructuredReport(
for (let i = 0; i < imageIds.length; i++) {
const imageId = imageIds[i];
const { SeriesInstanceUID, StudyInstanceUID } = cornerstone.metaData.get(
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get(
'instance',
imageId
);
@@ -106,43 +119,49 @@ export default function _hydrateStructuredReport(
}
}
Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
const toolDataForToolType = hydratableMeasurementsInSR[toolType];
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
const toolDataForAnnotationType =
hydratableMeasurementsInSR[annotationType];
toolDataForToolType.forEach(data => {
toolDataForAnnotationType.forEach(toolData => {
// Add the measurement to toolState
const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
const imageId = sopInstanceUIDToImageId[toolData.sopInstanceUid];
data.id = guid();
toolData.uid = guid();
const instance = cornerstone.metaData.get('instance', imageId);
const instance = metaData.get('instance', imageId);
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
// SOPInstanceUID,
// SeriesInstanceUID,
// StudyInstanceUID,
} = instance;
// Let the measurement service know we added to toolState
const toMeasurementSchema = getCornerstoneToolStateToMeasurementSchema(
toolType,
MeasurementService,
DisplaySetService,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID
const annotation = {
annotationUID: toolData.annotation.annotationUID,
data: toolData.annotation.data,
metadata: {
toolName: annotationType,
referencedImageId: imageId,
FrameOfReferenceUID,
},
};
const source = MeasurementService.getSource(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
);
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
const source = MeasurementService.getSource('CornerstoneTools', '4');
data.label = getLabelFromDCMJSImportedToolData(data);
const matchingMapping = mappings.find(
m => m.annotationType === annotationType
);
MeasurementService.addRawMeasurement(
source,
toolType,
data,
toMeasurementSchema,
annotationType,
{ annotation },
matchingMapping.toMeasurementSchema,
dataSource
);
@@ -159,3 +178,66 @@ export default function _hydrateStructuredReport(
SeriesInstanceUIDs,
};
}
function _mapLegacyDataSet(dataset) {
const REPORT = 'Imaging Measurements';
const GROUP = 'Measurement Group';
const TRACKING_IDENTIFIER = 'Tracking Identifier';
// Identify the Imaging Measurements
const imagingMeasurementContent = toArray(dataset.ContentSequence).find(
codeMeaningEquals(REPORT)
);
// Retrieve the Measurements themselves
const measurementGroups = toArray(
imagingMeasurementContent.ContentSequence
).filter(codeMeaningEquals(GROUP));
// For each of the supported measurement types, compute the measurement data
const measurementData = {};
const cornerstoneToolClasses =
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
const registeredToolClasses = [];
Object.keys(cornerstoneToolClasses).forEach(key => {
registeredToolClasses.push(cornerstoneToolClasses[key]);
measurementData[key] = [];
});
measurementGroups.forEach((measurementGroup, index) => {
const measurementGroupContentSequence = toArray(
measurementGroup.ContentSequence
);
const TrackingIdentifierGroup = measurementGroupContentSequence.find(
contentItem =>
contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
);
const TrackingIdentifier = TrackingIdentifierGroup.TextValue;
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
cornerstoneTag = CORNERSTONE_3D_TAG;
}
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
TrackingIdentifierGroup.TextValue = mappedTrackingIdentifier;
});
return dataset;
}
const toArray = function(x) {
return Array.isArray(x) ? x : [x];
};
const codeMeaningEquals = codeMeaningName => {
return contentItem => {
return contentItem.ConceptNameCodeSequence.CodeMeaning === codeMeaningName;
};
};
@@ -1,283 +0,0 @@
export default function getCornerstoneToolStateToMeasurementSchema(
toolType,
MeasurementService,
DisplaySetService,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID
) {
const _getValueTypeFromToolType = toolType => {
const {
POLYLINE,
ELLIPSE,
POINT,
BIDIRECTIONAL,
} = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attemped, but its not nearly flexible enough.
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
// You can't define a bidirectional tool as a single type..
// OHIF-230
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
};
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
};
switch (toolType) {
case 'Length':
return measurementData =>
Length(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
case 'Bidirectional':
return measurementData =>
Bidirectional(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
case 'EllipticalRoi':
return measurementData =>
EllipticalRoi(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
case 'ArrowAnnotate':
return measurementData =>
ArrowAnnotate(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
);
}
}
function Length(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
SeriesInstanceUID,
SOPInstanceUID
);
const { handles, label } = measurementData;
const points = [];
Object.keys(handles).map(handle => {
if (['start', 'end'].includes(handle)) {
let point = {};
if (handles[handle].x) point.x = handles[handle].x;
if (handles[handle].y) point.y = handles[handle].y;
points.push(point);
}
});
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID,
description: measurementData.description,
unit: measurementData.unit,
length: measurementData.length,
type: _getValueTypeFromToolType(tool),
points,
label,
};
}
function Bidirectional(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
SeriesInstanceUID,
SOPInstanceUID
);
const { handles, label } = measurementData;
const longAxis = [handles.start, handles.end];
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID,
description: measurementData.description,
unit: measurementData.unit,
shortestDiameter: measurementData.shortestDiameter,
longestDiameter: measurementData.longestDiameter,
type: _getValueTypeFromToolType(tool),
points: { longAxis, shortAxis },
label,
};
}
function EllipticalRoi(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
SeriesInstanceUID,
SOPInstanceUID
);
const { handles, label } = measurementData;
const { start, end } = handles;
const halfXLength = Math.abs(start.x - end.x) / 2;
const halfYLength = Math.abs(start.y - end.y) / 2;
const points = [];
const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
// To store similar to SR.
if (halfXLength > halfYLength) {
// X-axis major
// Major axis
points.push({ x: center.x - halfXLength, y: center.y });
points.push({ x: center.x + halfXLength, y: center.y });
// Minor axis
points.push({ x: center.x, y: center.y - halfYLength });
points.push({ x: center.x, y: center.y + halfYLength });
} else {
// Y-axis major
// Major axis
points.push({ x: center.x, y: center.y - halfYLength });
points.push({ x: center.x, y: center.y + halfYLength });
// Minor axis
points.push({ x: center.x - halfXLength, y: center.y });
points.push({ x: center.x + halfXLength, y: center.y });
}
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID,
description: measurementData.description,
unit: measurementData.unit,
area:
measurementData.cachedStats &&
measurementData.cachedStats
.area /* TODO: Add concept names instead (descriptor) */,
type: _getValueTypeFromToolType(tool),
points,
label,
};
}
function ArrowAnnotate(
measurementData,
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
DisplaySetService,
_getValueTypeFromToolType
) {
const tool = measurementData.toolType || measurementData.toolName;
const displaySetInstanceUID = _getDisplaySetInstanceUID(
DisplaySetService,
SeriesInstanceUID,
SOPInstanceUID
);
const { handles } = measurementData;
const points = [];
Object.keys(handles).map(handle => {
if (['start', 'end'].includes(handle)) {
let point = {};
if (handles[handle].x) point.x = handles[handle].x;
if (handles[handle].y) point.y = handles[handle].y;
points.push(point);
}
});
return {
id: measurementData.id,
SOPInstanceUID: SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID,
label: measurementData.text,
description: measurementData.description,
unit: measurementData.unit,
text: measurementData.text,
type: _getValueTypeFromToolType(tool),
points,
};
}
function _getDisplaySetInstanceUID(
DisplaySetService,
SeriesInstanceUID,
SOPInstanceUID
) {
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
return displaySet.displaySetInstanceUID;
}
@@ -2,4 +2,4 @@ export {
TrackedMeasurementsContext,
TrackedMeasurementsContextProvider,
useTrackedMeasurements,
} from './TrackedMeasurementsContext.jsx';
} from './TrackedMeasurementsContext.tsx';
@@ -44,7 +44,7 @@ const machineConfiguration = {
target: 'promptHydrateStructuredReport',
cond: 'hasNotIgnoredSRSeriesForHydration',
},
RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport'
RESTORE_PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport',
},
},
promptBeginTracking: {
@@ -224,7 +224,7 @@ const machineConfiguration = {
{
target: 'idle',
actions: ['ignoreHydrationForSRSeries'],
cond: 'shouldIgnoreHydrationForSR'
cond: 'shouldIgnoreHydrationForSR',
},
],
onError: {
@@ -308,7 +308,10 @@ const defaultOptions = {
ignoredSeries: [...ctx.ignoredSeries, evt.data.SeriesInstanceUID],
})),
ignoreHydrationForSRSeries: assign((ctx, evt) => ({
ignoredSRSeriesForHydration: [...ctx.ignoredSRSeriesForHydration, evt.data.srSeriesInstanceUID],
ignoredSRSeriesForHydration: [
...ctx.ignoredSRSeriesForHydration,
evt.data.srSeriesInstanceUID,
],
})),
addTrackedSeries: assign((ctx, evt) => ({
prevTrackedSeries: [...ctx.trackedSeries],
@@ -376,7 +379,7 @@ const defaultOptions = {
ctx.trackedSeries.length > 1 ||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
hasNotIgnoredSRSeriesForHydration: (ctx, evt) => {
return !ctx.ignoredSRSeriesForHydration.includes(evt.SeriesInstanceUID)
return !ctx.ignoredSRSeriesForHydration.includes(evt.SeriesInstanceUID);
},
isNewStudy: (ctx, evt) =>
!ctx.ignoredSeries.includes(evt.SeriesInstanceUID) &&
@@ -6,7 +6,7 @@ const RESPONSE = {
SET_STUDY_AND_SERIES: 3,
};
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
@@ -33,7 +33,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
id: 'prompt-begin-tracking-cancel',
type: 'cancel',
text: 'No',
value: RESPONSE.CANCEL
value: RESPONSE.CANCEL,
},
{
id: 'prompt-begin-tracking-no-do-not-ask-again',
@@ -68,4 +68,4 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
});
}
export default promptUser;
export default promptBeginTracking;
@@ -10,10 +10,19 @@ const RESPONSE = {
HYDRATE_REPORT: 5,
};
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService, DisplaySetService } = servicesManager.services;
function promptHydrateStructuredReport(
{ servicesManager, extensionManager },
ctx,
evt
) {
const {
UIViewportDialogService,
DisplaySetService,
} = servicesManager.services;
const { viewportIndex, displaySetInstanceUID } = evt;
const srDisplaySet = DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
const srDisplaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
return new Promise(async function(resolve, reject) {
const promptResult = await _askTrackMeasurements(
@@ -81,4 +90,4 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
});
}
export default promptUser;
export default promptHydrateStructuredReport;
@@ -1,9 +1,13 @@
import createReportAsync from './../../_shared/createReportAsync.js';
import createReportAsync from './../../_shared/createReportAsync';
import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
import getNextSRSeriesNumber from '../../_shared/getNextSRSeriesNumber';
import RESPONSE from '../../_shared/PROMPT_RESPONSES';
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
function promptSaveReport(
{ servicesManager, commandsManager, extensionManager },
ctx,
evt
) {
const {
UIDialogService,
MeasurementService,
@@ -45,6 +49,7 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) {
displaySetInstanceUIDs = await createReportAsync(
servicesManager,
commandsManager,
dataSource,
trackedMeasurements,
{
@@ -67,4 +72,4 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) {
});
}
export default promptUser;
export default promptSaveReport;
@@ -7,7 +7,7 @@ const RESPONSE = {
NO_NOT_FOR_SERIES: 4,
};
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
function promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
@@ -105,4 +105,4 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
});
}
export default promptUser;
export default promptTrackNewSeries;
@@ -7,7 +7,7 @@ const RESPONSE = {
NO_NOT_FOR_SERIES: 4,
};
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
function promptTrackNewStudy({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
@@ -104,4 +104,4 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
});
}
export default promptUser;
export default promptTrackNewStudy;
@@ -4,10 +4,14 @@ import {
useTrackedMeasurements,
} from './contexts';
function getContextModule({ servicesManager, extensionManager }) {
function getContextModule({
servicesManager,
extensionManager,
commandsManager,
}) {
const BoundTrackedMeasurementsContextProvider = TrackedMeasurementsContextProvider.bind(
null,
{ servicesManager, extensionManager }
{ servicesManager, extensionManager, commandsManager }
);
return [
@@ -1,6 +1,6 @@
import {
PanelStudyBrowserTracking,
PanelMeasurementTableTracking,
PanelStudyBrowserTracking,
} from './panels';
// TODO:
@@ -1,9 +1,9 @@
import getContextModule from './getContextModule.js';
import getPanelModule from './getPanelModule.js';
import getViewportModule from './getViewportModule.js';
import getContextModule from './getContextModule';
import getPanelModule from './getPanelModule';
import getViewportModule from './getViewportModule';
import { id } from './id.js';
export default {
const measurementTrackingExtension = {
/**
* Only required property. Should be a unique value across all extensions.
*/
@@ -34,3 +34,5 @@ export default {
};
},
};
export default measurementTrackingExtension;
@@ -11,10 +11,8 @@ import { DicomMetadataStore, utils } from '@ohif/core';
import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule';
import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
import RESPONSES from '../../_shared/PROMPT_RESPONSES';
import downloadCSVReport from '../../_shared/downloadCSVReport';
const { downloadCSVReport } = utils;
const { formatDate } = utils;
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
@@ -146,29 +144,24 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
trackedSeries.includes(m.referenceSeriesUID)
);
downloadCSVReport(
trackedMeasurements,
trackedStudy,
trackedSeries,
MeasurementService
);
downloadCSVReport(trackedMeasurements, MeasurementService);
}
const jumpToImage = ({ id, isActive }) => {
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, id);
const jumpToImage = ({ uid, isActive }) => {
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
onMeasurementItemClickHandler({ id, isActive });
onMeasurementItemClickHandler({ uid, isActive });
};
const onMeasurementItemEditHandler = ({ id, isActive }) => {
const measurement = MeasurementService.getMeasurement(id);
jumpToImage({ id, isActive });
const onMeasurementItemEditHandler = ({ uid, isActive }) => {
const measurement = MeasurementService.getMeasurement(uid);
jumpToImage({ uid, isActive });
const onSubmitHandler = ({ action, value }) => {
switch (action.id) {
case 'save': {
MeasurementService.update(
id,
uid,
{
...measurement,
...value,
@@ -225,12 +218,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
});
};
const onMeasurementItemClickHandler = ({ id, isActive }) => {
const onMeasurementItemClickHandler = ({ uid, isActive }) => {
if (!isActive) {
const measurements = [...displayMeasurements];
const measurement = measurements.find(m => m.id === id);
const measurement = measurements.find(m => m.uid === uid);
measurements.forEach(m => (m.isActive = m.id !== id ? false : true));
measurements.forEach(m => (m.isActive = m.uid !== uid ? false : true));
measurement.isActive = true;
setDisplayMeasurements(measurements);
}
@@ -245,8 +238,10 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
return (
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
data-cy={"trackedMeasurements-panel"}>
<div
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
data-cy={'trackedMeasurements-panel'}
>
{displayStudySummary.key && (
<StudySummary
date={formatDate(displayStudySummary.date)}
@@ -320,17 +315,9 @@ function _mapMeasurementToDisplay(measurement, types, DisplaySetService) {
);
}
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
const displayText = _getDisplayText(
measurement,
PixelSpacing,
SeriesNumber,
InstanceNumber,
types
);
const { displayText } = measurement;
return {
id: measurement.id,
uid: measurement.uid,
label: measurement.label || '(empty)',
measurementType: measurement.type,
displayText: displayText || [],
@@ -338,67 +325,4 @@ function _mapMeasurementToDisplay(measurement, types, DisplaySetService) {
};
}
/**
*
* @param {*} measurement
* @param {*} pixelSpacing
* @param {*} seriesNumber
* @param {*} instanceNumber
* @param {*} types
*/
function _getDisplayText(
measurement,
pixelSpacing,
seriesNumber,
instanceNumber,
types
) {
const { type, points } = measurement;
const hasPixelSpacing =
pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) &&
pixelSpacing.length === 2;
const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing
? pixelSpacing
: [1, 1];
const unit = hasPixelSpacing ? 'mm' : 'px';
switch (type) {
case types.POLYLINE: {
const { length } = measurement;
const roundedLength = _round(length, 2);
return [
`${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
];
}
case types.BIDIRECTIONAL: {
const { shortestDiameter, longestDiameter } = measurement;
const roundedShortestDiameter = _round(shortestDiameter, 1);
const roundedLongestDiameter = _round(longestDiameter, 1);
return [
`L: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`,
`W: ${roundedShortestDiameter} ${unit}`,
];
}
case types.ELLIPSE: {
const { area } = measurement;
const roundedArea = _round(area, 2);
return [
`${roundedArea} ${unit}<sup>2</sup> (S:${seriesNumber}, I:${instanceNumber})`,
];
}
case types.POINT: {
const { text } = measurement; // Will display in "short description"
return [`(S:${seriesNumber}, I:${instanceNumber})`];
}
}
}
function _round(value, decimals) {
return parseFloat(value).toFixed(decimals);
}
export default PanelMeasurementTableTracking;
@@ -47,14 +47,14 @@ function PanelStudyBrowserTracking({
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
viewportGridService.setDisplaysetForViewport({
viewportGridService.setDisplaySetsForViewport({
viewportIndex: activeViewportIndex,
displaySetInstanceUID,
displaySetInstanceUIDs: [displaySetInstanceUID],
});
};
const activeDisplaySetInstanceUID =
viewports[activeViewportIndex]?.displaySetInstanceUID;
const activeViewportDisplaySetInstanceUIDs =
viewports[activeViewportIndex]?.displaySetInstanceUIDs;
const isSingleViewport = numCols === 1 && numRows === 1;
@@ -260,8 +260,12 @@ function PanelStudyBrowserTracking({
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
if (!shouldCollapseStudy) {
const madeInClient = true
requestDisplaySetCreationForStudy(DisplaySetService, StudyInstanceUID, madeInClient);
const madeInClient = true;
requestDisplaySetCreationForStudy(
DisplaySetService,
StudyInstanceUID,
madeInClient
);
}
}
@@ -331,7 +335,7 @@ function PanelStudyBrowserTracking({
}}
onClickThumbnail={() => {}}
onDoubleClickThumbnail={onDoubleClickThumbnailHandler}
activeDisplaySetInstanceUID={activeDisplaySetInstanceUID}
activeDisplaySetInstanceUIDs={activeViewportDisplaySetInstanceUIDs}
/>
);
}
@@ -398,8 +402,12 @@ function _mapDisplaySets(
const viewportIdentificator = isSingleViewport
? []
: viewports.reduce((acc, viewportData, index) => {
if (viewportData.displaySetInstanceUID === ds.displaySetInstanceUID) {
acc.push(_viewportLabels[index]);
if (
viewportData?.displaySetInstanceUIDs?.includes(
ds.displaySetInstanceUID
)
) {
acc.push(viewportData.viewportLabel);
}
return acc;
}, []);
@@ -4,12 +4,10 @@
*/
function getImageSrcFromImageId(cornerstone, imageId) {
return new Promise((resolve, reject) => {
cornerstone
.loadAndCacheImage(imageId)
.then(image => {
const canvas = document.createElement('canvas');
cornerstone.renderToCanvas(canvas, image);
const canvas = document.createElement('canvas');
cornerstone.utilities
.loadImageToCanvas(canvas, imageId)
.then(imageId => {
resolve(canvas.toDataURL());
})
.catch(reject);
@@ -1,29 +0,0 @@
async function getStudiesForPatientByStudyInstanceUID(
dataSource,
StudyInstanceUID
) {
if (StudyInstanceUID === undefined) {
return;
}
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore?
// Kind of like promise fulfillment in cornerstone-wado-image-loader when there are multiple
// outgoing requests for the same data
const getStudyResult = await dataSource.query.studies.search({
studyInstanceUid: StudyInstanceUID,
});
// TODO: To Erik's point, the data source likely shouldn't deviate from
// Naturalized DICOM JSON when returning. It makes things like this awkward (mrn)
if (getStudyResult && getStudyResult.length && getStudyResult[0].mrn) {
return dataSource.query.studies.search({
patientId: getStudyResult[0].mrn,
});
}
console.log('No mrn found for', getStudyResult);
// The original study we KNOW belongs to the same set, so just return it
return getStudyResult;
}
export default getStudiesForPatientByStudyInstanceUID;
@@ -3,9 +3,17 @@ import PropTypes from 'prop-types';
//
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
import getImageSrcFromImageId from './getImageSrcFromImageId';
import getStudiesForPatientByStudyInstanceUID from './getStudiesForPatientByStudyInstanceUID';
import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
function _getStudyForPatientUtility(extensionManager) {
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-default.utilityModule.common'
);
const { getStudiesForPatientByStudyInstanceUID } = utilityModule.exports;
return getStudiesForPatientByStudyInstanceUID;
}
/**
* Wraps the PanelStudyBrowser and provides features afforded by managers/services
*
@@ -19,12 +27,16 @@ function WrappedPanelStudyBrowserTracking({
servicesManager,
}) {
const dataSource = extensionManager.getActiveDataSource()[0];
const getStudiesForPatientByStudyInstanceUID = _getStudyForPatientUtility(
extensionManager
);
const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind(
null,
dataSource
);
const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn(
commandsManager.getCommand.bind(commandsManager)
extensionManager
);
const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind(
null,
@@ -56,14 +68,13 @@ function WrappedPanelStudyBrowserTracking({
* @returns {func} getImageSrcFromImageId - A utility function powered by
* cornerstone
*/
function _createGetImageSrcFromImageIdFn(getCommand) {
try {
const command = getCommand('getCornerstoneLibraries', 'VIEWER');
if (!command) {
return;
}
const { cornerstone } = command.commandFn();
function _createGetImageSrcFromImageIdFn(extensionManager) {
const utilities = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
try {
const { cornerstone } = utilities.exports.getCornerstoneLibraries();
return getImageSrcFromImageId.bind(null, cornerstone);
} catch (ex) {
throw new Error('Required command not found');
@@ -1,378 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF, { utils } from '@ohif/core';
import {
Notification,
ViewportActionBar,
useCine,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
import { useTrackedMeasurements } from './../getContextModule';
import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive';
import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement';
import getTools from '../_shared/getTools';
const { formatDate } = utils;
// TODO -> Get this list from the list of tracked measurements.
// TODO -> We can now get a list of tool names from the measurement service.
// Use the toolnames to check which tools we have instead, using the
// Classes isn't really extensible unless we add the classes to the measurement
// Service definition, which feels wrong.
const {
ArrowAnnotateTool,
BidirectionalTool,
EllipticalRoiTool,
LengthTool,
} = cornerstoneTools;
const BaseAnnotationTool = cornerstoneTools.importInternal(
'base/BaseAnnotationTool'
);
const { StackManager } = OHIF.utils;
function TrackedCornerstoneViewport(props) {
const {
children,
dataSource,
displaySet,
viewportIndex,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { ToolBarService } = servicesManager.services;
const [trackedMeasurements] = useTrackedMeasurements();
const [
{ activeViewportIndex, viewports },
viewportGridService,
] = useViewportGrid();
const [{ isCineEnabled, cines }, cineService] = useCine();
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [isTracked, setIsTracked] = useState(false);
const [trackedMeasurementId, setTrackedMeasurementId] = useState(null);
const [element, setElement] = useState(null);
const onElementEnabled = evt => {
const eventData = evt.detail;
const targetElement = eventData.element;
const tools = getTools();
const toolAlias = ToolBarService.state.primaryToolId;
// Activate appropriate tool bindings for element
setActiveAndPassiveToolsForElement(targetElement, tools);
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
mouseButtonMask: 1,
});
// Set dashed, based on tracking, for this viewport
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(
tool => tool.element === targetElement
);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
} else if (tool instanceof BaseAnnotationTool) {
const configuration = tool.configuration;
configuration.renderDashed = true;
tool.configuration = configuration;
}
});
// Update image after setting tool config
const enabledElement = cornerstone.getEnabledElement(targetElement);
if (enabledElement.image) {
cornerstone.updateImage(targetElement);
}
setElement(targetElement);
const OHIFCornerstoneEnabledElementEvent = new CustomEvent(
'ohif-cornerstone-enabled-element-event',
{
detail: {
context: 'ACTIVE_VIEWPORT::TRACKED',
enabledElement: targetElement,
viewportIndex,
},
}
);
document.dispatchEvent(OHIFCornerstoneEnabledElementEvent);
};
useEffect(() => {
if (!element) {
return;
}
const allTools = cornerstoneTools.store.state.tools;
const toolsForElement = allTools.filter(tool => tool.element === element);
toolsForElement.forEach(tool => {
if (
tool instanceof ArrowAnnotateTool ||
tool instanceof BidirectionalTool ||
tool instanceof EllipticalRoiTool ||
tool instanceof LengthTool
) {
const configuration = tool.configuration;
configuration.renderDashed = !isTracked;
tool.configuration = configuration;
}
});
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement.image) {
cornerstone.updateImage(element);
}
}, [isTracked]);
// We have...
// StudyInstanceUid, DisplaySetInstanceUid
// Use displaySetInstanceUid --> SeriesInstanceUid
// Get meta for series, map to actionBar
// const displaySet = DisplaySetService.getDisplaySetByUID(
// dSet.displaySetInstanceUID
// );
// TODO: This display contains the meta for all instances.
// That can't be right...
// console.log('DISPLAYSET', displaySet);
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
// console.log(seriesMeta);
// TODO: Share this logic so it isn't out of sync where we retrieve
const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex(
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
);
const { trackedSeries } = trackedMeasurements.context;
const {
Modality,
SeriesDate,
SeriesDescription,
SeriesInstanceUID,
SeriesNumber,
} = displaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
SpacingBetweenSlices,
ManufacturerModelName,
} = displaySet.images[0];
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
const label = viewports.length > 1 ? _viewportLabels[viewportIndex] : '';
function switchMeasurement(direction) {
if (!element) {
// Element not yet enabled.
return;
}
const newTrackedMeasurementId = _getNextMeasurementId(
direction,
servicesManager,
trackedMeasurementId,
trackedMeasurements
);
if (!newTrackedMeasurementId) {
return;
}
setTrackedMeasurementId(newTrackedMeasurementId);
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const measurement = measurements.find(
m => m.id === newTrackedMeasurementId
);
setCornerstoneMeasurementActive(measurement);
MeasurementService.jumpToMeasurement(
viewportIndex,
newTrackedMeasurementId
);
}
const renderViewport = () => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
return (
<Component
onElementEnabled={onElementEnabled}
element={element}
{...props}
></Component>
);
};
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onSeriesChange={direction => switchMeasurement(direction)}
studyData={{
label,
isTracked,
isLocked: false,
isRehydratable: false,
studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok?
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
seriesDescription: SeriesDescription,
modality: Modality,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness
? `${parseFloat(SliceThickness).toFixed(2)}mm`
: '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '',
},
}}
showNavArrows={!isCineEnabled}
showCine={isCineEnabled}
cineProps={{
isPlaying,
onClose: () => commandsManager.runCommand('toggleCine'),
onPlayPauseChange: isPlaying =>
cineService.setCine({ id: activeViewportIndex, isPlaying }),
onFrameRateChange: frameRate =>
cineService.setCine({ id: activeViewportIndex, frameRate }),
}}
/>
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full overflow-hidden">
{renderViewport()}
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
id={viewportDialogState.id}
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
/>
)}
</div>
</div>
</>
);
}
TrackedCornerstoneViewport.propTypes = {
displaySet: PropTypes.object.isRequired,
viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
TrackedCornerstoneViewport.defaultProps = {
customProps: {},
};
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
function _getNextMeasurementId(
direction,
servicesManager,
trackedMeasurementId,
trackedMeasurements
) {
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const { trackedSeries } = trackedMeasurements.context;
// Get the potentially trackable measurements for this series,
// The measurements to jump between are the same
// regardless if this series is tracked or not.
const filteredMeasurements = measurements.filter(m =>
trackedSeries.includes(m.referenceSeriesUID)
);
if (!filteredMeasurements.length) {
// No measurements on this series.
return;
}
const measurementCount = filteredMeasurements.length;
const ids = filteredMeasurements.map(fm => fm.id);
let measurementIndex = ids.findIndex(id => id === trackedMeasurementId);
if (measurementIndex === -1) {
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
measurementIndex = 0;
} else {
if (direction === 'left') {
measurementIndex--;
if (measurementIndex < 0) {
measurementIndex = measurementCount - 1;
}
} else if (direction === 'right') {
measurementIndex++;
if (measurementIndex === measurementCount) {
measurementIndex = 0;
}
}
}
const newTrackedMeasurementId = ids[measurementIndex];
return newTrackedMeasurementId;
}
export default TrackedCornerstoneViewport;
@@ -0,0 +1,296 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import OHIF, { utils } from '@ohif/core';
import {
Notification,
ViewportActionBar,
useCine,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
import { annotation } from '@cornerstonejs/tools';
import { useTrackedMeasurements } from './../getContextModule';
const { formatDate } = utils;
function TrackedCornerstoneViewport(props) {
const {
children,
displaySets,
viewportIndex,
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const {
MeasurementService,
CornerstoneViewportService,
} = servicesManager.services;
// Todo: handling more than one displaySet on the same viewport
const displaySet = displaySets[0];
const [trackedMeasurements] = useTrackedMeasurements();
const [{ activeViewportIndex }] = useViewportGrid();
const [{ isCineEnabled, cines }, cineService] = useCine();
const [viewportDialogState] = useViewportDialog();
const [isTracked, setIsTracked] = useState(false);
const [trackedMeasurementUID, setTrackedMeasurementUID] = useState(null);
const [element, setElement] = useState(null);
const { trackedSeries } = trackedMeasurements.context;
const viewportId = CornerstoneViewportService.getViewportId(viewportIndex);
const {
Modality,
SeriesDate,
SeriesDescription,
SeriesInstanceUID,
SeriesNumber,
} = displaySet;
const {
PatientID,
PatientName,
PatientSex,
PatientAge,
SliceThickness,
SpacingBetweenSlices,
ManufacturerModelName,
} = displaySet.images[0];
useEffect(() => {
if (isTracked) {
annotation.config.style.setViewportToolStyles(viewportId, {
global: {
lineDash: '',
},
});
CornerstoneViewportService.getRenderingEngine().renderViewport(
viewportId
);
return;
}
annotation.config.style.setViewportToolStyles(`viewport-${viewportIndex}`, {
global: {
lineDash: '4,4',
},
});
CornerstoneViewportService.getRenderingEngine().renderViewport(viewportId);
return () => {
annotation.config.style.setViewportToolStyles(viewportId, {});
};
}, [isTracked]);
useEffect(() => {
if (!cines || !cines[viewportIndex]) {
return;
}
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
const frameRate = (cine && cine.frameRate) || 24;
const validFrameRate = Math.max(frameRate, 1);
if (isPlaying) {
cineService.playClip(element, {
framesPerSecond: validFrameRate,
});
} else {
cineService.stopClip(element);
}
}, [cines, viewportIndex, cineService, element, displaySet]);
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
setIsTracked(!isTracked);
}
/**
* OnElementEnabled callback which is called after the cornerstoneExtension
* has enabled the element. Note: we delegate all the image rendering to
* cornerstoneExtension, so we don't need to do anything here regarding
* the image rendering, element enabling etc.
*/
const onElementEnabled = evt => {
setElement(evt.detail.element);
};
function switchMeasurement(direction) {
const newTrackedMeasurementUID = _getNextMeasurementUID(
direction,
servicesManager,
trackedMeasurementUID,
trackedMeasurements
);
if (!newTrackedMeasurementUID) {
return;
}
setTrackedMeasurementUID(newTrackedMeasurementUID);
MeasurementService.jumpToMeasurement(
viewportIndex,
newTrackedMeasurementUID
);
}
const getCornerstoneViewport = () => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
);
return <Component {...props} onElementEnabled={onElementEnabled} />;
};
const cine = cines[viewportIndex];
const isPlaying = (cine && cine.isPlaying) || false;
return (
<>
<ViewportActionBar
onDoubleClick={evt => {
evt.stopPropagation();
evt.preventDefault();
}}
onSeriesChange={direction => switchMeasurement(direction)}
studyData={{
label: viewportLabel,
isTracked,
isLocked: false,
isRehydratable: false,
studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok?
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
seriesDescription: SeriesDescription,
modality: Modality,
patientInformation: {
patientName: PatientName
? OHIF.utils.formatPN(PatientName.Alphabetic)
: '',
patientSex: PatientSex || '',
patientAge: PatientAge || '',
MRN: PatientID || '',
thickness: SliceThickness
? `${parseFloat(SliceThickness).toFixed(2)}mm`
: '',
spacing:
SpacingBetweenSlices !== undefined
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
: '',
scanner: ManufacturerModelName || '',
},
}}
showNavArrows={!isCineEnabled}
showCine={isCineEnabled}
cineProps={{
isPlaying,
onClose: () => commandsManager.runCommand('toggleCine'),
onPlayPauseChange: isPlaying =>
cineService.setCine({
id: activeViewportIndex,
isPlaying,
}),
onFrameRateChange: frameRate =>
cineService.setCine({
id: activeViewportIndex,
frameRate,
}),
}}
/>
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full overflow-hidden">
{getCornerstoneViewport()}
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
id={viewportDialogState.id}
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
/>
)}
</div>
</div>
</>
);
}
TrackedCornerstoneViewport.propTypes = {
displaySets: PropTypes.arrayOf(PropTypes.object.isRequired).isRequired,
viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
TrackedCornerstoneViewport.defaultProps = {
customProps: {},
};
function _getNextMeasurementUID(
direction,
servicesManager,
trackedMeasurementId,
trackedMeasurements
) {
const { MeasurementService } = servicesManager.services;
const measurements = MeasurementService.getMeasurements();
const { trackedSeries } = trackedMeasurements.context;
// Get the potentially trackable measurements for this series,
// The measurements to jump between are the same
// regardless if this series is tracked or not.
const filteredMeasurements = measurements.filter(m =>
trackedSeries.includes(m.referenceSeriesUID)
);
if (!filteredMeasurements.length) {
// No measurements on this series.
return;
}
const measurementCount = filteredMeasurements.length;
const uids = filteredMeasurements.map(fm => fm.uid);
let measurementIndex = uids.findIndex(uid => uid === trackedMeasurementId);
if (measurementIndex === -1) {
// Not tracking a measurement, or previous measurement now deleted, revert to 0.
measurementIndex = 0;
} else {
if (direction === 'left') {
measurementIndex--;
if (measurementIndex < 0) {
measurementIndex = measurementCount - 1;
}
} else if (direction === 'right') {
measurementIndex++;
if (measurementIndex === measurementCount) {
measurementIndex = 0;
}
}
}
const newTrackedMeasurementId = uids[measurementIndex];
return newTrackedMeasurementId;
}
export default TrackedCornerstoneViewport;
@@ -1,39 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const ViewportLoadingIndicator = ({ error }) => {
if (error) {
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="text-primary-light text-xl font-light">
<h4>Error Loading Image</h4>
<p>An error has occurred.</p>
<p>{error.message}</p>
</div>
</>
);
}
return (
<>
<div className="bg-black h-full w-full absolute opacity-50"></div>
<div className="absolute transparent w-full h-full flex items-center justify-center">
<p className="text-primary-light text-xl font-light">Loading...</p>
</div>
</>
);
};
ViewportLoadingIndicator.propTypes = {
percentComplete: PropTypes.number,
error: PropTypes.object,
};
ViewportLoadingIndicator.defaultProps = {
percentComplete: 0,
error: null,
};
export default ViewportLoadingIndicator;
@@ -1,95 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import classnames from 'classnames';
const ViewportOverlay = ({
imageId,
scale,
windowWidth,
windowCenter,
imageIndex,
stackSize,
activeTools,
}) => {
const topLeft = 'top-viewport left-viewport';
const topRight = 'top-viewport right-viewport-scrollbar';
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
const bottomLeft = 'bottom-viewport left-viewport';
const overlay = 'absolute pointer-events-none';
const isZoomActive = activeTools.includes('Zoom');
const isWwwcActive = activeTools.includes('Wwwc');
if (!imageId) {
return null;
}
// TODO: this component should be presentational only. Right now it has a weird dependency on Cornerstone
const generalImageModule =
cornerstone.metaData.get('generalImageModule', imageId) || {};
const { instanceNumber } = generalImageModule;
return (
<div className="text-primary-light">
<div
data-cy={'viewport-overlay-top-left'}
className={classnames(overlay, topLeft)}
>
{isZoomActive && (
<div className="flex flex-row">
<span className="mr-1">Zoom:</span>
<span className="font-light">{scale.toFixed(2)}x</span>
</div>
)}
{isWwwcActive && (
<div className="flex flex-row">
<span className="mr-1">W:</span>
<span className="ml-1 mr-2 font-light">
{windowWidth.toFixed(0)}
</span>
<span className="mr-1">L:</span>
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
</div>
)}
</div>
<div
data-cy={'viewport-overlay-top-right'}
className={classnames(overlay, topRight)}
>
{stackSize > 1 && (
<div className="flex flex-row">
<span className="mr-1">I:</span>
<span className="font-light">
{`${instanceNumber} (${imageIndex}/${stackSize})`}
</span>
</div>
)}
</div>
<div
data-cy={'viewport-overlay-bottom-right'}
className={classnames(overlay, bottomRight)}
></div>
<div
data-cy={'viewport-overlay-bottom-left'}
className={classnames(overlay, bottomLeft)}
></div>
</div>
);
};
ViewportOverlay.propTypes = {
scale: PropTypes.number.isRequired,
windowWidth: PropTypes.number.isRequired,
windowCenter: PropTypes.number.isRequired,
imageId: PropTypes.string.isRequired,
imageIndex: PropTypes.number.isRequired,
stackSize: PropTypes.number.isRequired,
activeTools: PropTypes.arrayOf(PropTypes.string),
};
ViewportOverlay.defaultProps = {
activeTools: [],
};
export default ViewportOverlay;