[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
@@ -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);
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
export default function getToolStateToCornerstoneMeasurementSchema(
|
||||
toolType,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
imageId
|
||||
) {
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
const {
|
||||
POLYLINE,
|
||||
ELLIPSE,
|
||||
POINT,
|
||||
BIDIRECTIONAL,
|
||||
} = MeasurementService.VALUE_TYPES;
|
||||
|
||||
// TODO -> I get why this was attemped, but its not nearly flexible enough.
|
||||
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
||||
// You can't define a bidirectional tool as a single type..
|
||||
// OHIF-230
|
||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||
Length: POLYLINE,
|
||||
EllipticalRoi: ELLIPSE,
|
||||
Bidirectional: BIDIRECTIONAL,
|
||||
ArrowAnnotate: POINT,
|
||||
};
|
||||
|
||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||
};
|
||||
|
||||
switch (toolType) {
|
||||
case 'Length':
|
||||
return measurementData =>
|
||||
Length(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
case 'Bidirectional':
|
||||
return measurementData =>
|
||||
Bidirectional(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
case 'EllipticalRoi':
|
||||
return measurementData =>
|
||||
EllipticalRoi(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
case 'ArrowAnnotate':
|
||||
return measurementData =>
|
||||
ArrowAnnotate(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Length(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
const { handles, label } = measurementData;
|
||||
|
||||
const points = [];
|
||||
Object.keys(handles).map(handle => {
|
||||
if (['start', 'end'].includes(handle)) {
|
||||
let point = {};
|
||||
if (handles[handle].x) point.x = handles[handle].x;
|
||||
if (handles[handle].y) point.y = handles[handle].y;
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
length: measurementData.length,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
function Bidirectional(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
const { handles, label } = measurementData;
|
||||
|
||||
const longAxis = [handles.start, handles.end];
|
||||
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||
|
||||
return {
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
shortestDiameter: measurementData.shortestDiameter,
|
||||
longestDiameter: measurementData.longestDiameter,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points: { longAxis, shortAxis },
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
function EllipticalRoi(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
const { handles, label } = measurementData;
|
||||
const { start, end } = handles;
|
||||
|
||||
const halfXLength = Math.abs(start.x - end.x) / 2;
|
||||
const halfYLength = Math.abs(start.y - end.y) / 2;
|
||||
|
||||
const points = [];
|
||||
const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
||||
|
||||
// To store similar to SR.
|
||||
if (halfXLength > halfYLength) {
|
||||
// X-axis major
|
||||
// Major axis
|
||||
points.push({ x: center.x - halfXLength, y: center.y });
|
||||
points.push({ x: center.x + halfXLength, y: center.y });
|
||||
// Minor axis
|
||||
points.push({ x: center.x, y: center.y - halfYLength });
|
||||
points.push({ x: center.x, y: center.y + halfYLength });
|
||||
} else {
|
||||
// Y-axis major
|
||||
// Major axis
|
||||
points.push({ x: center.x, y: center.y - halfYLength });
|
||||
points.push({ x: center.x, y: center.y + halfYLength });
|
||||
// Minor axis
|
||||
points.push({ x: center.x - halfXLength, y: center.y });
|
||||
points.push({ x: center.x + halfXLength, y: center.y });
|
||||
}
|
||||
|
||||
return {
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
area:
|
||||
measurementData.cachedStats &&
|
||||
measurementData.cachedStats
|
||||
.area /* TODO: Add concept names instead (descriptor) */,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points,
|
||||
label,
|
||||
};
|
||||
}
|
||||
|
||||
function ArrowAnnotate(
|
||||
measurementData,
|
||||
imageId,
|
||||
DisplaySetService,
|
||||
_getValueTypeFromToolType
|
||||
) {
|
||||
const tool = measurementData.toolType || measurementData.toolName;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = instance;
|
||||
|
||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
const { handles } = measurementData;
|
||||
|
||||
const points = [];
|
||||
Object.keys(handles).map(handle => {
|
||||
if (['start', 'end'].includes(handle)) {
|
||||
let point = {};
|
||||
if (handles[handle].x) point.x = handles[handle].x;
|
||||
if (handles[handle].y) point.y = handles[handle].y;
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: measurementData.id,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
label: measurementData.text,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
text: measurementData.text,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points,
|
||||
};
|
||||
}
|
||||
|
||||
function _getDisplaySetInstanceUID(
|
||||
DisplaySetService,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
) {
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
return displaySet.displaySetInstanceUID;
|
||||
}
|
||||
+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 = [
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Extracts the label from the toolData imported from dcmjs. We need to do this
|
||||
* as dcmjs does not depeend on OHIF/the MeasurementService, it just produces data for cornestoneTools.
|
||||
* This optional data is available for the consumer to process if they wish to.
|
||||
* @param {object} toolData The tooldata relating to the
|
||||
*
|
||||
* @returns {string} The extracted label.
|
||||
*/
|
||||
export default function getLabelFromDCMJSImportedToolData(toolData) {
|
||||
const { findingSites = [], finding } = toolData;
|
||||
|
||||
let freeTextLabel = findingSites.find(
|
||||
fs => fs.CodeValue === 'CORNERSTONEFREETEXT'
|
||||
);
|
||||
|
||||
if (freeTextLabel) {
|
||||
return freeTextLabel.CodeMeaning;
|
||||
}
|
||||
|
||||
if (finding && finding.CodeValue === 'CORNERSTONEFREETEXT') {
|
||||
return finding.CodeMeaning;
|
||||
}
|
||||
}
|
||||
@@ -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