[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:
1 parent
5f487840a1
commit
2a51d75f77
26 files changed
+948
-391
No files matched your search
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
+100
-1
@@ -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]}
|
||||
|
||||
+171
@@ -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);
|
||||
}
|
||||
File renamed without changes.
+151
-8
@@ -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),
|
||||
},
|
||||
};
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ function promptUser({ servicesManager }, ctx, evt) {
|
||||
userResponse: promptResult,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+82
@@ -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;
|
||||
+62
@@ -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;
|
||||
+5
-23
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
+13
-26
@@ -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 = [
|
||||
|
||||
File renamed without changes.
@@ -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>
|
||||
</>
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user