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:
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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
+11
-6
@@ -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
|
||||
File renamed without 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
|
||||
}
|
||||
});
|
||||
}
|
||||
+18
-16
@@ -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,
|
||||
};
|
||||
|
||||
+124
-42
@@ -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;
|
||||
};
|
||||
};
|
||||
-283
@@ -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';
|
||||
+7
-4
@@ -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) &&
|
||||
|
||||
+3
-3
@@ -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;
|
||||
+13
-4
@@ -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;
|
||||
+8
-3
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+6
-2
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
PanelStudyBrowserTracking,
|
||||
PanelMeasurementTableTracking,
|
||||
PanelStudyBrowserTracking,
|
||||
} from './panels';
|
||||
|
||||
// TODO:
|
||||
File renamed without changes.
+6
-4
@@ -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;
|
||||
File renamed without changes.
+18
-94
@@ -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;
|
||||
+17
-9
@@ -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
-6
@@ -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);
|
||||
|
||||
-29
@@ -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;
|
||||
+20
-9
@@ -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;
|
||||
Reference in new issue
Block a user