diff --git a/.eslintrc.json b/.eslintrc.json
index 329eb380b..c8ec9056f 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -12,10 +12,11 @@
"settings": {
"react": {
"version": "detect"
- }
+
},
"rules": {
- "react/jsx-props-no-spreading": "error"
+ "react/jsx-props-no-spreading": "error",
+ "react-hooks/exhaustive-deps": "false"
},
"globals": {
"cy": true,
diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js
index 792c5fc22..2d1470bdc 100644
--- a/extensions/cornerstone/src/init.js
+++ b/extensions/cornerstone/src/init.js
@@ -334,6 +334,12 @@ const _connectToolsToMeasurementService = (
}
}
+ /**
+ * When csTools fires a removed event, remove the same measurement
+ * from the measurement service
+ *
+ * @param {*} csToolsEvent
+ */
function removeMeasurement(csToolsEvent) {
console.log('~~ removeEvt', csToolsEvent);
try {
@@ -381,15 +387,42 @@ const _connectMeasurementServiceToTools = (
});
});
- /* TODO: Remove per measurement
- MeasurementService.subscribe(MEASUREMENT_REMOVED,
- ({ source, measurement }) => {
- if ([sourceId].includes(source.id)) {
- // const annotation = getAnnotation('Length', measurement.id);
- // iterate tool state
- }
+ // TODO: This is an unsafe delete
+ // Cornerstone-tools should probably expose a more generic "delete by id"
+ // And have toolState managers expose a method to find any of their toolState by ID
+ // --> csTools.deleteById --> internally checks all registered modules/managers?
+ //
+ // This implementation assumes a single globalImageIdSpecificToolStateManager
+ // It iterates all toolState for all toolTypes, and deletes any with a matching id
+ //
+ // Could potentially use "source" from event to determine tool type and skip some
+ // iterations?
+ MeasurementService.subscribe(
+ MEASUREMENT_REMOVED,
+ ({ source, measurement: removedMeasurementId }) => {
+ // THIS POINTS TO ORIGINAL; Not a copy
+ const imageIdSpecificToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
+
+ // ImageId -->
+ Object.keys(imageIdSpecificToolState).forEach(imageId => {
+ // ImageId --> Tool -->
+ Object.keys(imageIdSpecificToolState[imageId]).forEach(toolName => {
+ const toolState = imageIdSpecificToolState[imageId][toolName];
+
+ let annotationIndex = toolState.data.length - 1;
+ while (annotationIndex >= 0) {
+ const annotation = toolState.data[annotationIndex];
+
+ if (annotation.id === removedMeasurementId) {
+ toolState.data.splice(annotationIndex, 1);
+ }
+
+ annotationIndex--;
+ }
+ });
+ });
}
- ); */
+ );
};
// const {
diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js
index 43be17461..22ddc0ec2 100644
--- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js
+++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js
@@ -4,11 +4,15 @@ import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
-import { ViewportActionBar, useViewportGrid } from '@ohif/ui';
+import {
+ Notification,
+ ViewportActionBar,
+ useViewportGrid,
+ useViewportDialog,
+} from '@ohif/ui';
import TOOL_NAMES from './constants/toolNames';
import { adapters } from 'dcmjs';
-import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
-import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
+// import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
import id from './id';
const { formatDate } = utils;
@@ -30,6 +34,7 @@ function OHIFCornerstoneSRViewport({
}) {
const { DisplaySetService, MeasurementService } = servicesManager.services;
const [viewportGrid, viewportGridService] = useViewportGrid();
+ const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
const [viewportData, setViewportData] = useState(null);
@@ -141,14 +146,9 @@ function OHIFCornerstoneSRViewport({
const numMeasurements = displaySet.measurements.length;
setMeasurementCount(numMeasurements);
- }, [
- dataSource,
- displaySet,
- displaySet.StudyInstanceUID,
- displaySet.displaySetInstanceUID,
- ]);
+ }, [dataSource, displaySet]);
- const updateViewport = newMeasurementSelected => {
+ const updateViewport = useCallback(newMeasurementSelected => {
const {
StudyInstanceUID,
displaySetInstanceUID,
@@ -181,28 +181,25 @@ function OHIFCornerstoneSRViewport({
cornerstone.updateImage(element);
}
});
- };
+ });
- useEffect(() => {
- if (element !== null) {
- setTrackingUniqueIdentifiersForElement(element);
- }
- }, [
- dataSource,
- displaySet,
- displaySet.StudyInstanceUID,
- displaySet.displaySetInstanceUID,
- ]);
+ useEffect(
+ () => {
+ if (element !== null) {
+ setTrackingUniqueIdentifiersForElement(element);
+ }
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [dataSource, displaySet]
+ );
- useEffect(() => {
- updateViewport(measurementSelected);
- }, [
- dataSource,
- displaySet,
- displaySet.StudyInstanceUID,
- displaySet.displaySetInstanceUID,
- element,
- ]);
+ useEffect(
+ () => {
+ updateViewport(measurementSelected);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [dataSource, displaySet, element]
+ );
const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex(
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
@@ -273,147 +270,6 @@ function OHIFCornerstoneSRViewport({
updateViewport(newMeasurementSelected);
};
- function hydrateMeasurementService() {
- // TODO -> We should define a strict versioning somewhere.
- const mappings = MeasurementService.getSourceMappings(
- 'CornerstoneTools',
- '4'
- );
-
- if (!mappings || !mappings.length) {
- throw new Error(
- `Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.`
- );
- }
-
- const instance = DicomMetadataStore.getInstance(
- displaySet.StudyInstanceUID,
- displaySet.SeriesInstanceUID,
- displaySet.SOPInstanceUID
- );
-
- const { MeasurementReport } = adapters.Cornerstone;
-
- const sopInstanceUIDToImageId = {};
-
- displaySet.measurements.forEach(measurement => {
- const { ReferencedSOPInstanceUID, imageId } = measurement;
- if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
- sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
- }
- });
-
- // Use dcmjs to generate toolState.
- const storedMeasurementByToolType = MeasurementReport.generateToolState(
- instance
- );
-
- // Filter what is found by DICOM SR to measurements we support.
- const mappingDefinitions = mappings.map(m => m.definition);
- const hydratableMeasurementsInSR = {};
-
- Object.keys(storedMeasurementByToolType).forEach(key => {
- if (mappingDefinitions.includes(key)) {
- hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key];
- }
- });
-
- if (
- extensionManager.registeredExtensionIds.includes(
- MEASUREMENT_TRACKING_EXTENSION_ID
- )
- ) {
- // Set the series touched as tracked.
- const imageIds = [];
-
- Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
- const toolDataForToolType = hydratableMeasurementsInSR[toolType];
-
- toolDataForToolType.forEach(data => {
- // Add the measurement to toolState
- const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
-
- if (!imageIds.includes(imageId)) {
- imageIds.push(imageId);
- }
- });
- });
-
- let targetStudyInstanceUID;
- const SeriesInstanceUIDs = [];
-
- for (let i = 0; i < imageIds.length; i++) {
- const imageId = imageIds[0];
- const {
- SeriesInstanceUID,
- StudyInstanceUID,
- } = cornerstone.metaData.get('instance', imageId);
-
- if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
- SeriesInstanceUIDs.push(SeriesInstanceUID);
- }
-
- if (!targetStudyInstanceUID) {
- targetStudyInstanceUID = StudyInstanceUID;
- } else if (targetStudyInstanceUID !== StudyInstanceUID) {
- console.warn(
- 'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
- );
- }
- }
-
- sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', {
- StudyInstanceUID: targetStudyInstanceUID,
- SeriesInstanceUIDs,
- });
- }
-
- Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
- const toolDataForToolType = hydratableMeasurementsInSR[toolType];
-
- toolDataForToolType.forEach(data => {
- // Add the measurement to toolState
- const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
-
- data.id = guid();
-
- _addToolDataToCornerstoneTools(data, toolType, imageId);
-
- // Let the measurement service know we added to toolState
- const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
- toolType,
- MeasurementService,
- DisplaySetService,
- imageId
- );
-
- const source = MeasurementService.getSource('CornerstoneTools', '4');
-
- data.label = getLabelFromDCMJSImportedToolData(data);
-
- MeasurementService.addRawMeasurement(
- source,
- toolType,
- data,
- toMeasurementSchema
- );
-
- if (!imageIds.includes(imageId)) {
- imageIds.push(imageId);
- }
- });
- });
-
- displaySet.isHydrated = true;
-
- setIsHydrated(true);
-
- viewportGridService.setDisplaysetForViewport({
- viewportIndex: activeViewportIndex,
- displaySetInstanceUID: activeDisplaySetData.displaySetInstanceUID,
- });
- }
-
const label = viewports.length > 1 ? _viewportLabels[viewportIndex] : '';
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
@@ -426,7 +282,6 @@ function OHIFCornerstoneSRViewport({
evt.preventDefault();
}}
onSeriesChange={onMeasurementChange}
- onHydrationClick={hydrateMeasurementService}
studyData={{
label,
isTracked: false,
@@ -447,8 +302,8 @@ function OHIFCornerstoneSRViewport({
spacing:
PixelSpacing && PixelSpacing.length
? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(
- 2
- )}mm`
+ 2
+ )}mm`
: '',
scanner: ManufacturerModelName || '',
},
@@ -471,6 +326,17 @@ function OHIFCornerstoneSRViewport({
// seizure inducing strobe blinking effect
resizeRefreshRateMs={150}
/>
+
+ {viewportDialogState.viewportIndex === viewportIndex && (
+
+ )}
+
{childrenWithProps}
>
@@ -582,27 +448,6 @@ async function _getViewportAndActiveDisplaySetData(
return { viewportData, activeDisplaySetData };
}
-function _addToolDataToCornerstoneTools(data, toolType, imageId) {
- const toolState = globalImageIdSpecificToolStateManager.saveToolState();
-
- if (toolState[imageId] === undefined) {
- toolState[imageId] = {};
- }
-
- const imageIdToolState = toolState[imageId];
-
- // If we don't have tool state for this type of tool, add an empty object
- if (imageIdToolState[toolType] === undefined) {
- imageIdToolState[toolType] = {
- data: [],
- };
- }
-
- const toolData = imageIdToolState[toolType];
-
- toolData.data.push(data);
-}
-
function _onDoubleClick() {
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
'manipulatorState'
diff --git a/extensions/dicom-sr/src/init.js b/extensions/dicom-sr/src/init.js
index ca8a9610b..03d203167 100644
--- a/extensions/dicom-sr/src/init.js
+++ b/extensions/dicom-sr/src/init.js
@@ -14,7 +14,7 @@ const defaultConfig = {
/**
* @param {object} configuration
*/
-export default function init({ configuration = {}, servicesManager }) {
+export default function init({ configuration = {} }) {
const conifg = Object.assign({}, defaultConfig, configuration);
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
@@ -22,22 +22,4 @@ export default function init({ configuration = {}, servicesManager }) {
cornerstoneTools.register('module', id, dicomSRModule);
cornerstoneTools.addTool(DICOMSRDisplayTool);
cornerstoneTools.setToolEnabled(TOOL_NAMES.DICOM_SR_DISPLAY_TOOL);
-
- const { DisplaySetService } = servicesManager.services;
-
- DisplaySetService.subscribe(
- DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
- data => {
- const { displaySetsAdded, options } = data;
- displaySetsAdded.forEach(dSet => {
- if (options.madeInClient) {
- // Set just made displaySets as hydrated.
- if (!dSet.isLoaded) {
- dSet.load();
- }
- dSet.isHydrated = true;
- }
- });
- }
- );
}
diff --git a/extensions/measurement-tracking/src/_shared/PROMPT_RESPONSES.js b/extensions/measurement-tracking/src/_shared/PROMPT_RESPONSES.js
new file mode 100644
index 000000000..39e2e61f1
--- /dev/null
+++ b/extensions/measurement-tracking/src/_shared/PROMPT_RESPONSES.js
@@ -0,0 +1,10 @@
+const RESPONSE = {
+ NO_NEVER: -1,
+ CANCEL: 0,
+ CREATE_REPORT: 1,
+ ADD_SERIES: 2,
+ SET_STUDY_AND_SERIES: 3,
+ NO_NOT_FOR_SERIES: 4,
+};
+
+export default RESPONSE;
diff --git a/extensions/measurement-tracking/src/_shared/createReportAsync.js b/extensions/measurement-tracking/src/_shared/createReportAsync.js
index 5aca672db..b6115b57a 100644
--- a/extensions/measurement-tracking/src/_shared/createReportAsync.js
+++ b/extensions/measurement-tracking/src/_shared/createReportAsync.js
@@ -1,11 +1,24 @@
import React from 'react';
-import { DICOMSR } from '@ohif/core';
+import { DICOMSR, DicomMetadataStore } from '@ohif/core';
-async function createReportAsync(servicesManager, dataSource, measurements) {
+/**
+ *
+ * @param {*} servicesManager
+ * @param {*} dataSource
+ * @param {*} measurements
+ * @param {*} options
+ * @returns {string[]} displaySetInstanceUIDs
+ */
+async function createReportAsync(
+ servicesManager,
+ dataSource,
+ measurements,
+ options
+) {
const {
+ DisplaySetService,
UINotificationService,
UIDialogService,
- DisplaySetService,
} = servicesManager.services;
const loadingDialogId = UIDialogService.create({
showOverlay: true,
@@ -19,17 +32,24 @@ async function createReportAsync(servicesManager, dataSource, measurements) {
const naturalizedReport = await DICOMSR.storeMeasurements(
measurements,
dataSource,
- ['ArrowAnnotate']
+ ['ArrowAnnotate'],
+ options
);
- DisplaySetService.makeDisplaySets([naturalizedReport], {
- madeInClient: true,
- });
+ // The "Mode" route listens for DicomMetadataStore changes
+ // When a new instance is added, it listens and
+ // automatically calls makeDisplaySets
+ DicomMetadataStore.addInstances([naturalizedReport], true);
+
+ const displaySetInstanceUID = DisplaySetService.getMostRecentDisplaySet();
+
UINotificationService.show({
title: 'Create Report',
message: 'Measurements saved successfully',
type: 'success',
});
+
+ return [displaySetInstanceUID];
} catch (error) {
UINotificationService.show({
title: 'Create Report',
diff --git a/extensions/measurement-tracking/src/_shared/createReportDialogPrompt.js b/extensions/measurement-tracking/src/_shared/createReportDialogPrompt.js
new file mode 100644
index 000000000..b0c13a666
--- /dev/null
+++ b/extensions/measurement-tracking/src/_shared/createReportDialogPrompt.js
@@ -0,0 +1,79 @@
+/* eslint-disable react/display-name */
+import React from 'react';
+import { Dialog, Input } from '@ohif/ui';
+import RESPONSE from './PROMPT_RESPONSES';
+
+export default function createReportDialogPrompt(UIDialogService) {
+ return new Promise(function(resolve, reject) {
+ let dialogId = undefined;
+
+ const _handleClose = () => {
+ // Dismiss dialog
+ UIDialogService.dismiss({ id: dialogId });
+ // Notify of cancel action
+ resolve({ action: RESPONSE.CANCEL, value: undefined });
+ };
+
+ /**
+ *
+ * @param {string} param0.action - value of action performed
+ * @param {string} param0.value - value from input field
+ */
+ const _handleFormSubmit = ({ action, value }) => {
+ UIDialogService.dismiss({ id: dialogId });
+ switch (action.id) {
+ case 'save':
+ resolve({ action: RESPONSE.CREATE_REPORT, value: value.label });
+ break;
+ case 'cancel':
+ resolve({ action: RESPONSE.CANCEL, value: undefined });
+ break;
+ }
+ };
+
+ dialogId = UIDialogService.create({
+ centralize: true,
+ isDraggable: false,
+ content: Dialog,
+ useLastPosition: false,
+ showOverlay: true,
+ contentProps: {
+ title: 'Provide a name for your report',
+ value: { label: '' },
+ noCloseButton: true,
+ onClose: _handleClose,
+ actions: [
+ { id: 'cancel', text: 'Cancel', type: 'secondary' },
+ { id: 'save', text: 'Save', type: 'primary' },
+ ],
+ // TODO: Should be on button press...
+ onSubmit: _handleFormSubmit,
+ body: ({ value, setValue }) => {
+ const onChangeHandler = event => {
+ event.persist();
+ setValue(value => ({ ...value, label: event.target.value }));
+ };
+ const onKeyPressHandler = event => {
+ if (event.key === 'Enter') {
+ UIDialogService.dismiss({ id: dialogId });
+ resolve({ action: RESPONSE.CREATE_REPORT, value: value.label });
+ }
+ };
+ return (
+
+
+
+ );
+ },
+ },
+ });
+ });
+}
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx
index 3ff87f663..c3079b54c 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx
@@ -1,7 +1,8 @@
-import React, { useContext } from 'react';
+import React, { useCallback, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
+import { useViewportGrid } from '@ohif/ui';
import {
machineConfiguration,
defaultOptions,
@@ -9,6 +10,9 @@ import {
import promptBeginTracking from './promptBeginTracking';
import promptTrackNewSeries from './promptTrackNewSeries';
import promptTrackNewStudy from './promptTrackNewStudy';
+import promptSaveReport from './promptSaveReport';
+import promptHydrateStructuredReport from './promptHydrateStructuredReport';
+import hydrateStructuredReport from './_hydrateStructuredReport.js';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
@@ -22,7 +26,60 @@ function TrackedMeasurementsContextProvider(
{ servicesManager, extensionManager }, // Bound by consumer
{ children } // Component props
) {
+ const [viewportGrid, viewportGridService] = useViewportGrid();
+ const { activeViewportIndex, viewports } = viewportGrid;
+
const machineOptions = Object.assign({}, defaultOptions);
+ machineOptions.actions = Object.assign({}, machineOptions.actions, {
+ showSeriesInActiveViewport: (ctx, evt) => {
+ const { DisplaySetService } = servicesManager.services;
+ const displaySetsForHydratedSeries = DisplaySetService.getDisplaySetsForSeries(
+ ctx.trackedSeries[0]
+ );
+
+ if (displaySetsForHydratedSeries.length > 0) {
+ const firstDisplaySetInstanceUID =
+ displaySetsForHydratedSeries[0].displaySetInstanceUID;
+
+ viewportGridService.setDisplaysetForViewport({
+ viewportIndex: evt.data.viewportIndex,
+ displaySetInstanceUID: firstDisplaySetInstanceUID,
+ });
+ }
+ },
+ showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
+ if (evt.data.createdDisplaySetInstanceUIDs.length > 0) {
+ const StructuredReportDisplaySetInstanceUID =
+ evt.data.createdDisplaySetInstanceUIDs[0].displaySetInstanceUID;
+
+ viewportGridService.setDisplaysetForViewport({
+ viewportIndex: evt.data.viewportIndex,
+ displaySetInstanceUID: StructuredReportDisplaySetInstanceUID,
+ });
+ }
+ },
+ discardPreviouslyTrackedMeasurements: (ctx, evt) => {
+ const { MeasurementService } = servicesManager.services;
+ const measurements = MeasurementService.getMeasurements();
+ const filteredMeasurements = measurements.filter(ms =>
+ ctx.prevTrackedSeries.includes(ms.referenceSeriesUID)
+ );
+ const measurementIds = filteredMeasurements.map(fm => fm.id);
+
+ for (let i = 0; i < measurementIds.length; i++) {
+ MeasurementService.remove('app-source', measurementIds[i]);
+ }
+ },
+ clearAllMeasurements: (ctx, evt) => {
+ const { MeasurementService } = servicesManager.services;
+ const measurements = MeasurementService.getMeasurements();
+ const measurementIds = measurements.map(fm => fm.id);
+
+ for (let i = 0; i < measurementIds.length; i++) {
+ MeasurementService.remove('app-source', measurementIds[i]);
+ }
+ },
+ });
machineOptions.services = Object.assign({}, machineOptions.services, {
promptBeginTracking: promptBeginTracking.bind(null, {
servicesManager,
@@ -36,6 +93,13 @@ function TrackedMeasurementsContextProvider(
servicesManager,
extensionManager,
}),
+ promptSaveReport: promptSaveReport.bind(null, {
+ servicesManager,
+ extensionManager,
+ }),
+ promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
+ servicesManager,
+ }),
});
// TODO: IMPROVE
@@ -58,6 +122,41 @@ function TrackedMeasurementsContextProvider(
trackedMeasurementsService,
] = useMachine(measurementTrackingMachine);
+ // ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle
+ useEffect(() => {
+ if (viewports.length > 0) {
+ const activeViewport = viewports[activeViewportIndex];
+
+ if (!activeViewport || !activeViewport.displaySetInstanceUID) {
+ return;
+ }
+
+ const { DisplaySetService } = servicesManager.services;
+ const displaySet = DisplaySetService.getDisplaySetByUID(
+ activeViewport.displaySetInstanceUID
+ );
+
+ // Magic string
+ // load function added by our sopClassHandler module
+ if (
+ displaySet.SOPClassHandlerId ===
+ 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr' &&
+ !displaySet.isLocked
+ ) {
+ console.log('sending event...', trackedMeasurements);
+ sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', {
+ displaySetInstanceUID: displaySet.displaySetInstanceUID,
+ viewportIndex: activeViewportIndex,
+ });
+ }
+ }
+ }, [
+ activeViewportIndex,
+ sendTrackedMeasurementsEvent,
+ servicesManager.services,
+ viewports,
+ ]);
+
return (
We should define a strict versioning somewhere.
+ const mappings = MeasurementService.getSourceMappings(
+ 'CornerstoneTools',
+ '4'
+ );
+
+ if (!mappings || !mappings.length) {
+ throw new Error(
+ `Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.`
+ );
+ }
+
+ const instance = DicomMetadataStore.getInstance(
+ displaySet.StudyInstanceUID,
+ displaySet.SeriesInstanceUID,
+ displaySet.SOPInstanceUID
+ );
+
+ const { MeasurementReport } = adapters.Cornerstone;
+
+ const sopInstanceUIDToImageId = {};
+
+ displaySet.measurements.forEach(measurement => {
+ const { ReferencedSOPInstanceUID, imageId } = measurement;
+ if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
+ sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
+ }
+ });
+
+ // Use dcmjs to generate toolState.
+ const storedMeasurementByToolType = MeasurementReport.generateToolState(
+ instance
+ );
+
+ // Filter what is found by DICOM SR to measurements we support.
+ const mappingDefinitions = mappings.map(m => m.definition);
+ const hydratableMeasurementsInSR = {};
+
+ Object.keys(storedMeasurementByToolType).forEach(key => {
+ if (mappingDefinitions.includes(key)) {
+ hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key];
+ }
+ });
+
+ // Set the series touched as tracked.
+ const imageIds = [];
+
+ // TODO: notification if no hydratable?
+ Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
+ const toolDataForToolType = hydratableMeasurementsInSR[toolType];
+
+ toolDataForToolType.forEach(data => {
+ // Add the measurement to toolState
+ const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
+
+ if (!imageIds.includes(imageId)) {
+ imageIds.push(imageId);
+ }
+ });
+ });
+
+ let targetStudyInstanceUID;
+ const SeriesInstanceUIDs = [];
+
+ for (let i = 0; i < imageIds.length; i++) {
+ const imageId = imageIds[i];
+ const { SeriesInstanceUID, StudyInstanceUID } = cornerstone.metaData.get(
+ 'instance',
+ imageId
+ );
+
+ if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
+ SeriesInstanceUIDs.push(SeriesInstanceUID);
+ }
+
+ if (!targetStudyInstanceUID) {
+ targetStudyInstanceUID = StudyInstanceUID;
+ } else if (targetStudyInstanceUID !== StudyInstanceUID) {
+ console.warn(
+ 'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
+ );
+ }
+ }
+
+ Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
+ const toolDataForToolType = hydratableMeasurementsInSR[toolType];
+
+ toolDataForToolType.forEach(data => {
+ // Add the measurement to toolState
+ const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
+
+ data.id = guid();
+
+ _addToolDataToCornerstoneTools(data, toolType, imageId);
+
+ // Let the measurement service know we added to toolState
+ const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
+ toolType,
+ MeasurementService,
+ DisplaySetService,
+ imageId
+ );
+
+ const source = MeasurementService.getSource('CornerstoneTools', '4');
+
+ data.label = getLabelFromDCMJSImportedToolData(data);
+
+ MeasurementService.addRawMeasurement(
+ source,
+ toolType,
+ data,
+ toMeasurementSchema
+ );
+
+ if (!imageIds.includes(imageId)) {
+ imageIds.push(imageId);
+ }
+ });
+ });
+
+ displaySet.isHydrated = true;
+
+ return {
+ StudyInstanceUID: targetStudyInstanceUID,
+ SeriesInstanceUIDs,
+ };
+}
+
+function _addToolDataToCornerstoneTools(data, toolType, imageId) {
+ const toolState = globalImageIdSpecificToolStateManager.saveToolState();
+
+ if (toolState[imageId] === undefined) {
+ toolState[imageId] = {};
+ }
+
+ const imageIdToolState = toolState[imageId];
+
+ // If we don't have tool state for this type of tool, add an empty object
+ if (imageIdToolState[toolType] === undefined) {
+ imageIdToolState[toolType] = {
+ data: [],
+ };
+ }
+
+ const toolData = imageIdToolState[toolType];
+
+ toolData.data.push(data);
+}
diff --git a/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getToolStateToCornerstoneMeasurementSchema.js
similarity index 100%
rename from extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js
rename to extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/getToolStateToCornerstoneMeasurementSchema.js
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js
index a69e92835..1e2c5e6d3 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js
@@ -6,6 +6,8 @@ const RESPONSE = {
CREATE_REPORT: 1,
ADD_SERIES: 2,
SET_STUDY_AND_SERIES: 3,
+ NO_NOT_FOR_SERIES: 4,
+ HYDRATE_REPORT: 5,
};
const machineConfiguration = {
@@ -14,6 +16,11 @@ const machineConfiguration = {
context: {
trackedStudy: '',
trackedSeries: [],
+ ignoredSeries: [],
+ //
+ prevTrackedStudy: '',
+ prevTrackedSeries: [],
+ prevIgnoredSeries: [],
},
states: {
off: {
@@ -29,6 +36,7 @@ const machineConfiguration = {
actions: ['setTrackedStudyAndMultipleSeries'],
},
],
+ PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport',
},
},
promptBeginTracking: {
@@ -65,7 +73,6 @@ const machineConfiguration = {
cond: 'isNewSeries',
},
],
-
UNTRACK_SERIES: [
{
target: 'tracking',
@@ -82,6 +89,7 @@ const machineConfiguration = {
actions: ['setTrackedStudyAndMultipleSeries'],
},
],
+ SAVE_REPORT: 'promptSaveReport',
},
},
promptTrackNewSeries: {
@@ -95,10 +103,16 @@ const machineConfiguration = {
},
{
target: 'tracking',
- actions: ['setTrackedStudyAndSeries'],
+ actions: [
+ 'discardPreviouslyTrackedMeasurements',
+ 'setTrackedStudyAndSeries',
+ ],
cond: 'shouldSetStudyAndSeries',
},
- // CREATE_REPORT && CANCEL
+ {
+ target: 'promptSaveReport',
+ cond: 'shouldPromptSaveReport',
+ },
{
target: 'tracking',
},
@@ -114,11 +128,80 @@ const machineConfiguration = {
onDone: [
{
target: 'tracking',
- actions: ['setTrackedStudyAndSeries'],
+ actions: [
+ 'discardPreviouslyTrackedMeasurements',
+ 'setTrackedStudyAndSeries',
+ ],
cond: 'shouldSetStudyAndSeries',
},
{
target: 'tracking',
+ actions: ['ignoreSeries'],
+ cond: 'shouldAddIgnoredSeries',
+ },
+ {
+ target: 'promptSaveReport',
+ cond: 'shouldPromptSaveReport',
+ },
+ {
+ target: 'tracking',
+ },
+ ],
+ onError: {
+ target: 'idle',
+ },
+ },
+ },
+ promptSaveReport: {
+ invoke: {
+ src: 'promptSaveReport',
+ onDone: [
+ // "clicked the save button"
+ // - should clear all measurements
+ // - show DICOM SR
+ {
+ target: 'idle',
+ actions: [
+ 'clearAllMeasurements',
+ 'showStructuredReportDisplaySetInActiveViewport',
+ ],
+ cond: 'shouldSaveAndContinueWithSameReport',
+ },
+ // "starting a new report"
+ // - remove "just saved" measurements
+ // - start tracking a new study + report
+ {
+ target: 'tracking',
+ actions: [
+ 'discardPreviouslyTrackedMeasurements',
+ 'setTrackedStudyAndSeries',
+ ],
+ cond: 'shouldSaveAndStartNewReport',
+ },
+ // Cancel, back to tracking
+ {
+ target: 'tracking',
+ },
+ ],
+ onError: {
+ target: 'idle',
+ },
+ },
+ },
+ promptHydrateStructuredReport: {
+ invoke: {
+ src: 'promptHydrateStructuredReport',
+ onDone: [
+ {
+ target: 'tracking',
+ actions: [
+ 'setTrackedStudyAndMultipleSeries',
+ 'showSeriesInActiveViewport',
+ ],
+ cond: 'shouldHydrateStructuredReport',
+ },
+ {
+ target: 'idle',
},
],
onError: {
@@ -143,23 +226,66 @@ const defaultOptions = {
},
},
actions: {
+ discardPreviouslyTrackedMeasurements: (ctx, evt) => {
+ console.log('discardPreviouslyTrackedMeasurements: not implemented');
+ },
+ clearAllMeasurements: (ctx, evt) => {
+ console.log('clearAllMeasurements: not implemented');
+ },
+ showSeriesInActiveViewport: (ctx, evt) => {
+ console.warn('showSeriesInActiveViewport: not implemented');
+ },
+ showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
+ console.warn(
+ 'showStructuredReportDisplaySetInActiveViewport: not implemented'
+ );
+ },
clearContext: assign({
trackedStudy: '',
trackedSeries: [],
+ ignoredSeries: [],
+ prevTrackedStudy: '',
+ prevTrackedSeries: [],
+ prevIgnoredSeries: [],
}),
// Promise resolves w/ `evt.data.*`
setTrackedStudyAndSeries: assign((ctx, evt) => ({
+ prevTrackedStudy: ctx.trackedStudy,
+ prevTrackedSeries: ctx.trackedSeries.slice(),
+ prevIgnoredSeries: ctx.ignoredSeries.slice(),
+ //
trackedStudy: evt.data.StudyInstanceUID,
trackedSeries: [evt.data.SeriesInstanceUID],
+ ignoredSeries: [],
})),
- setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({
- trackedStudy: evt.StudyInstanceUID,
- trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs],
+ setTrackedStudyAndMultipleSeries: assign((ctx, evt) => {
+ const studyInstanceUID =
+ evt.StudyInstanceUID || evt.data.StudyInstanceUID;
+ const seriesInstanceUIDs =
+ evt.SeriesInstanceUIDs || evt.data.SeriesInstanceUIDs;
+
+ return {
+ prevTrackedStudy: ctx.trackedStudy,
+ prevTrackedSeries: ctx.trackedSeries.slice(),
+ prevIgnoredSeries: ctx.ignoredSeries.slice(),
+ //
+ trackedStudy: studyInstanceUID,
+ trackedSeries: [...ctx.trackedSeries, ...seriesInstanceUIDs],
+ ignoredSeries: [],
+ };
+ }),
+ ignoreSeries: assign((ctx, evt) => ({
+ prevIgnoredSeries: [...ctx.ignoredSeries],
+ ignoredSeries: [...ctx.ignoredSeries, evt.data.SeriesInstanceUID],
})),
addTrackedSeries: assign((ctx, evt) => ({
+ prevTrackedSeries: [...ctx.trackedSeries],
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
})),
removeTrackedSeries: assign((ctx, evt) => ({
+ prevTrackedSeries: ctx.trackedSeries
+ .slice()
+ .filter(ser => ser !== evt.SeriesInstanceUID),
trackedSeries: ctx.trackedSeries
.slice()
.filter(ser => ser !== evt.SeriesInstanceUID),
@@ -172,13 +298,30 @@ const defaultOptions = {
evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES,
shouldSetStudyAndSeries: (ctx, evt) =>
evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES,
+ shouldAddIgnoredSeries: (ctx, evt) =>
+ evt.data && evt.data.userResponse === RESPONSE.NO_NOT_FOR_SERIES,
+ shouldPromptSaveReport: (ctx, evt) =>
+ evt.data && evt.data.userResponse === RESPONSE.CREATE_REPORT,
+ shouldSaveAndContinueWithSameReport: (ctx, evt) =>
+ evt.data &&
+ evt.data.userResponse === RESPONSE.CREATE_REPORT &&
+ evt.data.isBackupSave === true,
+ shouldSaveAndStartNewReport: (ctx, evt) =>
+ evt.data &&
+ evt.data.userResponse === RESPONSE.CREATE_REPORT &&
+ evt.data.isBackupSave === false,
+ shouldHydrateStructuredReport: (ctx, evt) =>
+ evt.data && evt.data.userResponse === RESPONSE.HYDRATE_REPORT,
// Has more than 1, or SeriesInstanceUID is not in list
// --> Post removal would have non-empty trackedSeries array
hasRemainingTrackedSeries: (ctx, evt) =>
ctx.trackedSeries.length > 1 ||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
- isNewStudy: (ctx, evt) => ctx.trackedStudy !== evt.StudyInstanceUID,
+ isNewStudy: (ctx, evt) =>
+ !ctx.ignoredSeries.includes(evt.SeriesInstanceUID) &&
+ ctx.trackedStudy !== evt.StudyInstanceUID,
isNewSeries: (ctx, evt) =>
+ !ctx.ignoredSeries.includes(evt.SeriesInstanceUID) &&
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
},
};
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js
index 57a6188fd..b4a66ba73 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js
@@ -20,6 +20,7 @@ function promptUser({ servicesManager }, ctx, evt) {
userResponse: promptResult,
StudyInstanceUID,
SeriesInstanceUID,
+ viewportIndex,
});
});
}
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js
new file mode 100644
index 000000000..4d017cf10
--- /dev/null
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptHydrateStructuredReport.js
@@ -0,0 +1,82 @@
+import hydrateStructuredReport from './_hydrateStructuredReport.js';
+
+const RESPONSE = {
+ NO_NEVER: -1,
+ CANCEL: 0,
+ CREATE_REPORT: 1,
+ ADD_SERIES: 2,
+ SET_STUDY_AND_SERIES: 3,
+ NO_NOT_FOR_SERIES: 4,
+ HYDRATE_REPORT: 5,
+};
+
+function promptUser({ servicesManager, extensionManager }, ctx, evt) {
+ const { UIViewportDialogService } = servicesManager.services;
+ const { viewportIndex, displaySetInstanceUID } = evt;
+
+ return new Promise(async function(resolve, reject) {
+ const promptResult = await _askTrackMeasurements(
+ UIViewportDialogService,
+ viewportIndex
+ );
+
+ // Need to do action here... So we can set state...
+ let StudyInstanceUID, SeriesInstanceUIDs;
+
+ if (promptResult === RESPONSE.HYDRATE_REPORT) {
+ console.warn('!! HYDRATING STRUCTURED REPORT');
+ const hydrationResult = hydrateStructuredReport(
+ { servicesManager, extensionManager },
+ displaySetInstanceUID
+ );
+
+ StudyInstanceUID = hydrationResult.StudyInstanceUID;
+ SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs;
+ }
+
+ resolve({
+ userResponse: promptResult,
+ displaySetInstanceUID: evt.displaySetInstanceUID,
+ viewportIndex,
+ StudyInstanceUID,
+ SeriesInstanceUIDs,
+ });
+ });
+}
+
+function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
+ return new Promise(function(resolve, reject) {
+ const message =
+ 'Do you want to continue tracking measurements for this study?';
+ const actions = [
+ {
+ type: 'secondary',
+ text: 'No',
+ value: RESPONSE.CANCEL,
+ },
+ {
+ type: 'primary',
+ text: 'Yes',
+ value: RESPONSE.HYDRATE_REPORT,
+ },
+ ];
+ const onSubmit = result => {
+ UIViewportDialogService.hide();
+ resolve(result);
+ };
+
+ UIViewportDialogService.show({
+ viewportIndex,
+ type: 'info',
+ message,
+ actions,
+ onSubmit,
+ onOutsideClick: () => {
+ UIViewportDialogService.hide();
+ resolve(RESPONSE.CANCEL);
+ },
+ });
+ });
+}
+
+export default promptUser;
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js
new file mode 100644
index 000000000..e1afa48d8
--- /dev/null
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptSaveReport.js
@@ -0,0 +1,62 @@
+import createReportAsync from './../../_shared/createReportAsync.js';
+import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
+import RESPONSE from '../../_shared/PROMPT_RESPONSES';
+
+function promptUser({ servicesManager, extensionManager }, ctx, evt) {
+ const { UIDialogService, MeasurementService } = servicesManager.services;
+ const viewportIndex =
+ evt.viewportIndex === undefined
+ ? evt.data.viewportIndex
+ : evt.viewportIndex;
+ const isBackupSave =
+ evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
+ const StudyInstanceUID = evt?.data?.StudyInstanceUID;
+ const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
+
+ const { trackedStudy, trackedSeries } = ctx;
+ let displaySetInstanceUIDs;
+
+ return new Promise(async function(resolve, reject) {
+ // TODO: Fallback if (UIDialogService) {
+ const promptResult = await createReportDialogPrompt(UIDialogService);
+
+ if (promptResult.action === RESPONSE.CREATE_REPORT) {
+ const dataSources = extensionManager.getDataSources();
+ const dataSource = dataSources[0];
+ const measurements = MeasurementService.getMeasurements();
+ const trackedMeasurements = measurements.filter(
+ m =>
+ trackedStudy === m.referenceStudyUID &&
+ trackedSeries.includes(m.referenceSeriesUID)
+ );
+
+ const SeriesDescription =
+ // isUndefinedOrEmpty
+ promptResult.value === undefined || promptResult.value === ''
+ ? 'Research Derived Series' // default
+ : promptResult.value; // provided value
+
+ displaySetInstanceUIDs = await createReportAsync(
+ servicesManager,
+ dataSource,
+ trackedMeasurements,
+ {
+ SeriesDescription,
+ }
+ );
+ } else if (promptResult.action === RESPONSE.CANCEL) {
+ // Do nothing
+ }
+
+ resolve({
+ userResponse: promptResult.action,
+ createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
+ StudyInstanceUID,
+ SeriesInstanceUID,
+ viewportIndex,
+ isBackupSave,
+ });
+ });
+}
+
+export default promptUser;
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js
index 2370ef65d..bbadc45ff 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js
@@ -1,20 +1,15 @@
-import createReportAsync from './../../_shared/createReportAsync.js';
-
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
CREATE_REPORT: 1,
ADD_SERIES: 2,
SET_STUDY_AND_SERIES: 3,
+ NO_NOT_FOR_SERIES: 4,
};
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
- const {
- UIViewportDialogService,
- MeasurementService,
- } = servicesManager.services;
+ const { UIViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
- const { trackedStudy, trackedSeries } = ctx;
return new Promise(async function(resolve, reject) {
let promptResult = await _askShouldAddMeasurements(
@@ -29,25 +24,12 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) {
);
}
- if (promptResult === RESPONSE.CREATE_REPORT) {
- // TODO -> Eventually deal with multiple dataSources.
- // Would need some way of saying which one is the "push" dataSource
- const dataSources = extensionManager.getDataSources();
- const dataSource = dataSources[0];
- const measurements = MeasurementService.getMeasurements();
- const trackedMeasurements = measurements.filter(
- m =>
- trackedStudy === m.referenceStudyUID &&
- trackedSeries.includes(m.referenceSeriesUID)
- );
-
- createReportAsync(servicesManager, dataSource, trackedMeasurements);
- }
-
resolve({
userResponse: promptResult,
StudyInstanceUID,
SeriesInstanceUID,
+ viewportIndex,
+ isBackupSave: false,
});
});
}
@@ -96,7 +78,7 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
{ type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
{
type: 'secondary',
- text: 'Save in report',
+ text: 'Save',
value: RESPONSE.CREATE_REPORT,
},
{
diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js
index e99da9179..cfcbd79e7 100644
--- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js
+++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js
@@ -1,22 +1,17 @@
-import createReportAsync from './../../_shared/createReportAsync.js';
-
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
CREATE_REPORT: 1,
ADD_SERIES: 2,
SET_STUDY_AND_SERIES: 3,
+ NO_NOT_FOR_SERIES: 4,
};
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
- const {
- UIViewportDialogService,
- MeasurementService,
- } = servicesManager.services;
+ const { UIViewportDialogService } = servicesManager.services;
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
- const { trackedStudy, trackedSeries } = ctx;
- return new Promise(async function (resolve, reject) {
+ return new Promise(async function(resolve, reject) {
let promptResult = await _askTrackMeasurements(
UIViewportDialogService,
viewportIndex
@@ -29,34 +24,26 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) {
);
}
- if (promptResult === RESPONSE.CREATE_REPORT) {
- // TODO -> Eventually deal with multiple dataSources.
- // Would need some way of saying which one is the "push" dataSource
- const dataSources = extensionManager.getDataSources();
- const dataSource = dataSources[0];
- const measurements = MeasurementService.getMeasurements();
- const trackedMeasurements = measurements.filter(
- m =>
- trackedStudy === m.referenceStudyUID &&
- trackedSeries.includes(m.referenceSeriesUID)
- );
-
- createReportAsync(servicesManager, dataSource, trackedMeasurements);
- }
-
resolve({
userResponse: promptResult,
StudyInstanceUID,
SeriesInstanceUID,
+ viewportIndex,
+ isBackupSave: false,
});
});
}
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
- return new Promise(function (resolve, reject) {
- const message = 'Track measurements for this series?';
+ return new Promise(function(resolve, reject) {
+ const message = 'Track measurements for this seies?';
const actions = [
{ type: 'cancel', text: 'No', value: RESPONSE.CANCEL },
+ {
+ type: 'secondary',
+ text: 'No, do not ask again for this series',
+ value: RESPONSE.NO_NOT_FOR_SERIES,
+ },
{
type: 'primary',
text: 'Yes',
@@ -83,7 +70,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
}
function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
- return new Promise(function (resolve, reject) {
+ return new Promise(function(resolve, reject) {
const message =
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?';
const actions = [
diff --git a/extensions/dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/utils/getLabelFromDCMJSImportedToolData.js
similarity index 100%
rename from extensions/dicom-sr/src/utils/getLabelFromDCMJSImportedToolData.js
rename to extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/utils/getLabelFromDCMJSImportedToolData.js
diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
index af89f39c7..614231ff4 100644
--- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
+++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js
@@ -11,8 +11,8 @@ import { DicomMetadataStore, DICOMSR, utils } from '@ohif/core';
import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule';
-import createReportAsync from './../../_shared/createReportAsync.js';
-import setCornerstoneMeasurementActive from '../../_shared/setCornerstoneMeasurementActive';
+import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
+import RESPONSES from '../../_shared/PROMPT_RESPONSES';
const { formatDate } = utils;
@@ -32,11 +32,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
measurementChangeTimestamp,
200
);
- const {
- MeasurementService,
- UIDialogService,
- DisplaySetService,
- } = servicesManager.services;
+ const { MeasurementService, UIDialogService } = servicesManager.services;
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
@@ -132,11 +128,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
};
}, [MeasurementService, sendTrackedMeasurementsEvent]);
- function createReport() {
- // TODO -> Eventually deal with multiple dataSources.
- // Would need some way of saying which one is the "push" dataSource
- const dataSources = extensionManager.getDataSources();
- const dataSource = dataSources[0];
+ async function exportReport() {
const measurements = MeasurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m =>
@@ -144,20 +136,21 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
trackedSeries.includes(m.referenceSeriesUID)
);
- return createReportAsync(servicesManager, dataSource, trackedMeasurements);
- }
+ const promptResult = await createReportDialogPrompt(UIDialogService);
- function exportReport() {
- const measurements = MeasurementService.getMeasurements();
- const trackedMeasurements = measurements.filter(
- m =>
- trackedStudy === m.referenceStudyUID &&
- trackedSeries.includes(m.referenceSeriesUID)
- );
+ if (promptResult.action === RESPONSES.CREATE_REPORT) {
+ const additionalFindings = ['ArrowAnnotate'];
- const additionalFindings = ['ArrowAnnotate'];
+ const SeriesDescription =
+ // isUndefinedOrEmpty
+ promptResult.value === undefined || promptResult.value === ''
+ ? 'Research Derived Series' // default
+ : promptResult.value; // provided value
- DICOMSR.downloadReport(trackedMeasurements, additionalFindings);
+ DICOMSR.downloadReport(trackedMeasurements, additionalFindings, {
+ SeriesDescription,
+ });
+ }
}
const jumpToImage = ({ id, isActive }) => {
@@ -273,7 +266,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
{
+ sendTrackedMeasurementsEvent('SAVE_REPORT', {
+ viewportIndex: viewportGrid.activeViewportIndex,
+ isBackupSave: true,
+ });
+ }}
/>
>
diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx
index 515ab7c71..3a8b6b93f 100644
--- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx
+++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx
@@ -448,7 +448,7 @@ function _createStudyBrowserTabs(
ds => ds.StudyInstanceUID === study.studyInstanceUid
);
- displaySetsForStudy.sort((a,b) => {
+ displaySetsForStudy.sort((a, b) => {
if (a.seriesNumber !== b.seriesNumber) {
return a.seriesNumber - b.seriesNumber;
}
diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js
index 817d80482..cce0f881b 100644
--- a/platform/core/src/DICOMSR/dataExchange.js
+++ b/platform/core/src/DICOMSR/dataExchange.js
@@ -6,6 +6,7 @@ import {
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
+import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import dcmjs from 'dcmjs';
@@ -47,11 +48,20 @@ const retrieveMeasurements = server => {
*
* @param {object[]} measurementData An array of measurements from the measurements service
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
+ * @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
* as opposed to Finding Sites.
* that you wish to serialize.
*/
-const downloadReport = (measurementData, additionalFindingTypes = []) => {
- const srDataset = generateReport(measurementData, additionalFindingTypes);
+const downloadReport = (
+ measurementData,
+ additionalFindingTypes,
+ options = {}
+) => {
+ const srDataset = generateReport(
+ measurementData,
+ additionalFindingTypes,
+ options
+ );
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
//Create a URL for the binary.
@@ -64,19 +74,29 @@ const downloadReport = (measurementData, additionalFindingTypes = []) => {
* @param {object[]} measurementData An array of measurements from the measurements service
* that you wish to serialize.
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
+ * @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
+ *
*/
-const generateReport = (measurementData, additionalFindingTypes) => {
+const generateReport = (
+ measurementData,
+ additionalFindingTypes,
+ options = {}
+) => {
const filteredToolState = _getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
);
-
const report = MeasurementReport.generateReport(
filteredToolState,
cornerstone.metaData
);
- return report.dataset;
+ const { dataset } = report;
+
+ // Add in top level series options
+ Object.assign(dataset, options);
+
+ return dataset;
};
/**
@@ -85,13 +105,14 @@ const generateReport = (measurementData, additionalFindingTypes) => {
* that you wish to serialize.
* @param {object} dataSource The dataSource that you wish to use to persist the data.
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
- * as opposed to Finding Sites.
+ * @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
* @return {object} The naturalized report
*/
const storeMeasurements = async (
measurementData,
dataSource,
- additionalFindingTypes = []
+ additionalFindingTypes,
+ options = {}
) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// But it is good enough for now whilst we only have cornerstone as a datasource.
@@ -105,7 +126,8 @@ const storeMeasurements = async (
try {
const naturalizedReport = generateReport(
measurementData,
- additionalFindingTypes
+ additionalFindingTypes,
+ options
);
const { StudyInstanceUID } = naturalizedReport;
@@ -117,6 +139,7 @@ const storeMeasurements = async (
return naturalizedReport;
} catch (error) {
+ console.warn(error);
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
@@ -124,6 +147,8 @@ const storeMeasurements = async (
}
};
+// _getFilteredCornerstoneToolState
+// DIFFERENT IMPLEMENTATION HERE! What's up?
function _getFilteredCornerstoneToolState(
measurementData,
additionalFindingTypes
@@ -147,7 +172,6 @@ function _getFilteredCornerstoneToolState(
}
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
-
const toolData = imageIdSpecificToolState[toolType].data;
let finding;
diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js
index 0993e31ef..07249389b 100644
--- a/platform/core/src/extensions/ExtensionManager.js
+++ b/platform/core/src/extensions/ExtensionManager.js
@@ -16,7 +16,7 @@ export default class ExtensionManager {
this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = [];
});
- this._extensionLifeCycleHooks = { onModeEnter: {} };
+ this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
this.dataSourceMap = {};
this.defaultDataSourceName = appConfig.defaultDataSourceName;
this.activeDataSource = undefined;
@@ -29,12 +29,21 @@ export default class ExtensionManager {
onModeEnter() {
const {
registeredExtensionIds,
- getModuleEntry,
_servicesManager,
_commandsManager,
_extensionLifeCycleHooks,
} = this;
+ const {
+ MeasurementService,
+ ViewportGridService,
+ HangingProtocolService,
+ } = _servicesManager.services;
+
+ MeasurementService.clearMeasurements();
+ ViewportGridService.reset();
+ HangingProtocolService.reset();
+
registeredExtensionIds.forEach(extensionId => {
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
@@ -47,6 +56,36 @@ export default class ExtensionManager {
});
}
+ onModeExit() {
+ const {
+ registeredExtensionIds,
+ _servicesManager,
+ _commandsManager,
+ _extensionLifeCycleHooks,
+ } = this;
+
+ const {
+ MeasurementService,
+ ViewportGridService,
+ HangingProtocolService,
+ } = _servicesManager.services;
+
+ MeasurementService.clearMeasurements();
+ ViewportGridService.reset();
+ HangingProtocolService.reset();
+
+ registeredExtensionIds.forEach(extensionId => {
+ const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
+
+ if (typeof onModeExit === 'function') {
+ onModeExit({
+ servicesManager: _servicesManager,
+ commandsManager: _commandsManager,
+ });
+ }
+ });
+ }
+
/**
* An array of extensions, or an array of arrays that contains extension
* configuration pairs.
@@ -110,6 +149,11 @@ export default class ExtensionManager {
extension.onModeEnter;
}
+ if (extension.onModeExit) {
+ this._extensionLifeCycleHooks.onModeExit[extensionId] =
+ extension.onModeExit;
+ }
+
// Register Modules
this.moduleTypeNames.forEach(moduleType => {
const extensionModule = this._getExtensionModule(
diff --git a/platform/core/src/measurements/conformance/ConformanceCriteria.js b/platform/core/src/measurements/conformance/ConformanceCriteria.js
index f600207eb..5fd817eca 100644
--- a/platform/core/src/measurements/conformance/ConformanceCriteria.js
+++ b/platform/core/src/measurements/conformance/ConformanceCriteria.js
@@ -77,10 +77,10 @@ class ConformanceCriteria {
this.nonconformities = nonconformities;
this.groupedNonConformities = groupedNonConformities;
- console.warn('nonconformities');
- console.warn(nonconformities);
- console.warn('groupedNonConformities');
- console.warn(groupedNonConformities);
+ // console.warn('nonconformities');
+ // console.warn(nonconformities);
+ // console.warn('groupedNonConformities');
+ // console.warn(groupedNonConformities);
return nonconformities;
}
diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js
index 58eee20f3..0a74ab380 100644
--- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js
+++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js
@@ -57,9 +57,7 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
const BaseImplementation = {
EVENTS,
listeners: {},
- // TODO: The assumption is that this is called per Study per Series
- // We should do more to verify/clarify that
- addInstances(instances) {
+ addInstances(instances, madeInClient = false) {
const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
let study = _model.studies.find(
@@ -81,6 +79,7 @@ const BaseImplementation = {
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
StudyInstanceUID,
SeriesInstanceUID,
+ madeInClient,
});
},
addSeriesMetadata(seriesSummaryMetadata) {
diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js
index 08026fe46..6db68a128 100644
--- a/platform/core/src/services/DisplaySetService/DisplaySetService.js
+++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js
@@ -36,6 +36,10 @@ export default class DisplaySetService {
return displaySetCache;
}
+ getMostRecentDisplaySet() {
+ return this.activeDisplaySets[this.activeDisplaySets.length - 1];
+ }
+
getActiveDisplaySets() {
return this.activeDisplaySets;
}
@@ -69,6 +73,12 @@ export default class DisplaySetService {
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUid
);
+ /**
+ *
+ * @param {*} input
+ * @param {*} param1
+ * @returns {string[]} - added displaySetInstanceUIDs
+ */
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
if (!input || !input.length) {
throw new Error('No instances were provided.');
@@ -84,11 +94,12 @@ export default class DisplaySetService {
let displaySetsAdded = [];
if (batch) {
- input.forEach(instances => {
+ for (let i = 0; i < input.length; i++) {
+ const instances = input[i];
const displaySets = this.makeDisplaySetForInstances(instances);
displaySetsAdded = [...displaySetsAdded, displaySets];
- });
+ }
} else {
const displaySets = this.makeDisplaySetForInstances(input);
@@ -109,6 +120,8 @@ export default class DisplaySetService {
displaySetsAdded,
options,
});
+
+ return displaySetsAdded;
}
};
diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx
index 0d58f5343..b31fd2e4c 100644
--- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx
+++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx
@@ -16,7 +16,6 @@ const ViewportActionBar = ({
showNavArrows,
showPatientInfo: patientInfoVisibility,
onSeriesChange,
- onHydrationClick,
onDoubleClick,
}) => {
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
@@ -32,7 +31,6 @@ const ViewportActionBar = ({
label,
isTracked,
isLocked,
- isHydrated,
modality,
studyDate,
currentSeries,
@@ -78,13 +76,7 @@ const ViewportActionBar = ({
Measurements cannot be duplicated.
)
- : () => (
-
- This SR is unlocked.
- You can duplicate measurements on your current report
by
- clicking 'Edit'.
-
- );
+ : () => This SR is unlocked.
;
return (
<>
} position="bottom-left">
@@ -101,16 +93,6 @@ const ViewportActionBar = ({
)}
- {!isLocked && !isHydrated && (
-
-
- Edit
-
-
- )}
>
);
}
diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx
index 64581131e..a851c0f24 100644
--- a/platform/viewer/src/routes/Mode/Mode.jsx
+++ b/platform/viewer/src/routes/Mode/Mode.jsx
@@ -104,14 +104,15 @@ export default function ModeRoute({
// TODO: For some reason this is running before the Providers
// are calling setServiceImplementation
// TOOD -> iterate through services.
- MeasurementService.clearMeasurements();
- ViewportGridService.reset();
- HangingProtocolService.reset();
// Extension
extensionManager.onModeEnter();
// Mode
route.init({ servicesManager, extensionManager });
+
+ return () => {
+ extensionManager.onModeExit();
+ };
}, [
mode,
dataSourceName,
@@ -131,13 +132,16 @@ export default function ModeRoute({
// DisplaySetService would wire this up?
const { unsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
- ({ StudyInstanceUID, SeriesInstanceUID }) => {
+ ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
- DisplaySetService.makeDisplaySets(seriesMetadata.instances);
+ DisplaySetService.makeDisplaySets(
+ seriesMetadata.instances,
+ madeInClient
+ );
}
);
@@ -161,9 +165,7 @@ export default function ModeRoute({
const { unsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => {
- const studyMetadata = DicomMetadataStore.getStudy(
- StudyInstanceUID,
- );
+ const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
const sortedSeries = studyMetadata.series.sort((a, b) => {
const aLowPriority = isLowPriorityModality(a.Modality);
@@ -176,13 +178,12 @@ export default function ModeRoute({
}
return a.SeriesNumber - b.SeriesNumber;
- })
+ });
- const { SeriesInstanceUID } = sortedSeries[0]
+ const { SeriesInstanceUID } = sortedSeries[0];
- HangingProtocolService.setHangingProtocol(
- {
- /*protocolMatchingRules: [
+ HangingProtocolService.setHangingProtocol({
+ /*protocolMatchingRules: [
{
id: '7tmuq7KzDMCWFeapc',
weight: 2,
@@ -195,9 +196,9 @@ export default function ModeRoute({
},
},
],*/
- stages: [
- {
- /*id: 'v5PfGt9F6mffZPif5',
+ stages: [
+ {
+ /*id: 'v5PfGt9F6mffZPif5',
viewportStructure: {
type: 'grid',
properties: {
@@ -206,30 +207,29 @@ export default function ModeRoute({
},
layoutTemplateName: 'gridLayout',
},*/
- viewports: [
- {
- viewportSettings: {},
- imageMatchingRules: [],
- seriesMatchingRules: [
- {
- id: 'mXnsCcNzZL56z7mTZ',
- weight: 1,
- required: true,
- attribute: 'SeriesInstanceUID',
- constraint: {
- equals: {
- value: SeriesInstanceUID,
- },
+ viewports: [
+ {
+ viewportSettings: {},
+ imageMatchingRules: [],
+ seriesMatchingRules: [
+ {
+ id: 'mXnsCcNzZL56z7mTZ',
+ weight: 1,
+ required: true,
+ attribute: 'SeriesInstanceUID',
+ constraint: {
+ equals: {
+ value: SeriesInstanceUID,
},
},
- ],
- studyMatchingRules: [],
- },
- ],
- }
- ]
- }
- )
+ },
+ ],
+ studyMatchingRules: [],
+ },
+ ],
+ },
+ ],
+ });
}
);
return unsubscribe;