[OHIF-339] - workflow changes (#1931)

* workflow changes

* Cherry pick OHIF-310 into this PR.

* fix label bug and dead code

* remove comment

* Wire up CreateReportButton

* fix additionalFindingTypes + save interaction

* Remove unused createReportAsync call

* stub for report hydration prompt

* Update PR

* Remove debuggers

* push updated code

* Push updates

* push

* fix sidebar save

* enable tracking of new study or series

* fix for duplicate created report series in sidebard

* fix for unexpected viewport dialog after visiting study list then a new study

* Fix for hydration not marking multiple series as active

* Reviewer changes.

* Respond to final review comment, add series description setting for export.

Co-authored-by: James A. Petts <jamesapetts@gmail.com>
This commit is contained in:
Danny BrownandJames A. Petts authored and GitHub committed 2020-08-04 11:11:23 +02:00
1 parent 5f487840a1
commit 2a51d75f77
26 files changed
+948 -391

No files matched your search

+41 -8
View File
@@ -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 {
@@ -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}
/>
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
onOutsideClick={viewportDialogState.onOutsideClick}
/>
)}
</div>
{childrenWithProps}
</div>
</>
@@ -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'
+1 -19
View File
@@ -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;
}
});
}
);
}
@@ -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;
@@ -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',
@@ -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 (
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
containerClassName="mr-2"
value={value.label}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
/>
</div>
);
},
},
});
});
}
@@ -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 (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
@@ -0,0 +1,171 @@
import cornerstoneTools from 'cornerstone-tools';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
import getToolStateToCornerstoneMeasurementSchema from './getToolStateToCornerstoneMeasurementSchema';
import { adapters } from 'dcmjs';
const { guid } = OHIF.utils;
const globalImageIdSpecificToolStateManager =
cornerstoneTools.globalImageIdSpecificToolStateManager;
/**
*
*/
export default function _hydrateStructuredReport(
{ servicesManager, extensionManager },
displaySetInstanceUID
) {
const { MeasurementService, DisplaySetService } = servicesManager.services;
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// 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];
}
});
// 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);
}
@@ -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),
},
};
@@ -20,6 +20,7 @@ function promptUser({ servicesManager }, ctx, evt) {
userResponse: promptResult,
StudyInstanceUID,
SeriesInstanceUID,
viewportIndex,
});
});
}
@@ -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;
@@ -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;
@@ -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,
},
{
@@ -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 = [
@@ -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 }) {
<div className="flex justify-center p-4">
<ActionButtons
onExportClick={exportReport}
onCreateReportClick={createReport}
onCreateReportClick={() => {
sendTrackedMeasurementsEvent('SAVE_REPORT', {
viewportIndex: viewportGrid.activeViewportIndex,
isBackupSave: true,
});
}}
/>
</div>
</>
@@ -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;
}