[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:
parent
5f487840a1
commit
2a51d75f77
@ -12,10 +12,11 @@
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
|
||||
},
|
||||
"rules": {
|
||||
"react/jsx-props-no-spreading": "error"
|
||||
"react/jsx-props-no-spreading": "error",
|
||||
"react-hooks/exhaustive-deps": "false"
|
||||
},
|
||||
"globals": {
|
||||
"cy": true,
|
||||
|
||||
@ -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>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useCallback, useContext, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Machine } from 'xstate';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import {
|
||||
machineConfiguration,
|
||||
defaultOptions,
|
||||
@ -9,6 +10,9 @@ import {
|
||||
import promptBeginTracking from './promptBeginTracking';
|
||||
import promptTrackNewSeries from './promptTrackNewSeries';
|
||||
import promptTrackNewStudy from './promptTrackNewStudy';
|
||||
import promptSaveReport from './promptSaveReport';
|
||||
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
||||
import hydrateStructuredReport from './_hydrateStructuredReport.js';
|
||||
|
||||
const TrackedMeasurementsContext = React.createContext();
|
||||
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
||||
@ -22,7 +26,60 @@ function TrackedMeasurementsContextProvider(
|
||||
{ servicesManager, extensionManager }, // Bound by consumer
|
||||
{ children } // Component props
|
||||
) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const { activeViewportIndex, viewports } = viewportGrid;
|
||||
|
||||
const machineOptions = Object.assign({}, defaultOptions);
|
||||
machineOptions.actions = Object.assign({}, machineOptions.actions, {
|
||||
showSeriesInActiveViewport: (ctx, evt) => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const displaySetsForHydratedSeries = DisplaySetService.getDisplaySetsForSeries(
|
||||
ctx.trackedSeries[0]
|
||||
);
|
||||
|
||||
if (displaySetsForHydratedSeries.length > 0) {
|
||||
const firstDisplaySetInstanceUID =
|
||||
displaySetsForHydratedSeries[0].displaySetInstanceUID;
|
||||
|
||||
viewportGridService.setDisplaysetForViewport({
|
||||
viewportIndex: evt.data.viewportIndex,
|
||||
displaySetInstanceUID: firstDisplaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
},
|
||||
showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
|
||||
if (evt.data.createdDisplaySetInstanceUIDs.length > 0) {
|
||||
const StructuredReportDisplaySetInstanceUID =
|
||||
evt.data.createdDisplaySetInstanceUIDs[0].displaySetInstanceUID;
|
||||
|
||||
viewportGridService.setDisplaysetForViewport({
|
||||
viewportIndex: evt.data.viewportIndex,
|
||||
displaySetInstanceUID: StructuredReportDisplaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
},
|
||||
discardPreviouslyTrackedMeasurements: (ctx, evt) => {
|
||||
const { MeasurementService } = servicesManager.services;
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const filteredMeasurements = measurements.filter(ms =>
|
||||
ctx.prevTrackedSeries.includes(ms.referenceSeriesUID)
|
||||
);
|
||||
const measurementIds = filteredMeasurements.map(fm => fm.id);
|
||||
|
||||
for (let i = 0; i < measurementIds.length; i++) {
|
||||
MeasurementService.remove('app-source', measurementIds[i]);
|
||||
}
|
||||
},
|
||||
clearAllMeasurements: (ctx, evt) => {
|
||||
const { MeasurementService } = servicesManager.services;
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const measurementIds = measurements.map(fm => fm.id);
|
||||
|
||||
for (let i = 0; i < measurementIds.length; i++) {
|
||||
MeasurementService.remove('app-source', measurementIds[i]);
|
||||
}
|
||||
},
|
||||
});
|
||||
machineOptions.services = Object.assign({}, machineOptions.services, {
|
||||
promptBeginTracking: promptBeginTracking.bind(null, {
|
||||
servicesManager,
|
||||
@ -36,6 +93,13 @@ function TrackedMeasurementsContextProvider(
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
promptSaveReport: promptSaveReport.bind(null, {
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
||||
servicesManager,
|
||||
}),
|
||||
});
|
||||
|
||||
// TODO: IMPROVE
|
||||
@ -58,6 +122,41 @@ function TrackedMeasurementsContextProvider(
|
||||
trackedMeasurementsService,
|
||||
] = useMachine(measurementTrackingMachine);
|
||||
|
||||
// ~~ Listen for changes to ViewportGrid for potential SRs hung in panes when idle
|
||||
useEffect(() => {
|
||||
if (viewports.length > 0) {
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
|
||||
if (!activeViewport || !activeViewport.displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Magic string
|
||||
// load function added by our sopClassHandler module
|
||||
if (
|
||||
displaySet.SOPClassHandlerId ===
|
||||
'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr' &&
|
||||
!displaySet.isLocked
|
||||
) {
|
||||
console.log('sending event...', trackedMeasurements);
|
||||
sendTrackedMeasurementsEvent('PROMPT_HYDRATE_SR', {
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
viewportIndex: activeViewportIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
activeViewportIndex,
|
||||
sendTrackedMeasurementsEvent,
|
||||
servicesManager.services,
|
||||
viewports,
|
||||
]);
|
||||
|
||||
return (
|
||||
<TrackedMeasurementsContext.Provider
|
||||
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
|
||||
|
||||
@ -0,0 +1,171 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
||||
import getToolStateToCornerstoneMeasurementSchema from './getToolStateToCornerstoneMeasurementSchema';
|
||||
import { adapters } from 'dcmjs';
|
||||
|
||||
const { guid } = OHIF.utils;
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function _hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const { MeasurementService, DisplaySetService } = servicesManager.services;
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// TODO -> We should define a strict versioning somewhere.
|
||||
const mappings = MeasurementService.getSourceMappings(
|
||||
'CornerstoneTools',
|
||||
'4'
|
||||
);
|
||||
|
||||
if (!mappings || !mappings.length) {
|
||||
throw new Error(
|
||||
`Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.`
|
||||
);
|
||||
}
|
||||
|
||||
const instance = DicomMetadataStore.getInstance(
|
||||
displaySet.StudyInstanceUID,
|
||||
displaySet.SeriesInstanceUID,
|
||||
displaySet.SOPInstanceUID
|
||||
);
|
||||
|
||||
const { MeasurementReport } = adapters.Cornerstone;
|
||||
|
||||
const sopInstanceUIDToImageId = {};
|
||||
|
||||
displaySet.measurements.forEach(measurement => {
|
||||
const { ReferencedSOPInstanceUID, imageId } = measurement;
|
||||
if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) {
|
||||
sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId;
|
||||
}
|
||||
});
|
||||
|
||||
// Use dcmjs to generate toolState.
|
||||
const storedMeasurementByToolType = MeasurementReport.generateToolState(
|
||||
instance
|
||||
);
|
||||
|
||||
// Filter what is found by DICOM SR to measurements we support.
|
||||
const mappingDefinitions = mappings.map(m => m.definition);
|
||||
const hydratableMeasurementsInSR = {};
|
||||
|
||||
Object.keys(storedMeasurementByToolType).forEach(key => {
|
||||
if (mappingDefinitions.includes(key)) {
|
||||
hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Set the series touched as tracked.
|
||||
const imageIds = [];
|
||||
|
||||
// TODO: notification if no hydratable?
|
||||
Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
|
||||
const toolDataForToolType = hydratableMeasurementsInSR[toolType];
|
||||
|
||||
toolDataForToolType.forEach(data => {
|
||||
// Add the measurement to toolState
|
||||
const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
|
||||
|
||||
if (!imageIds.includes(imageId)) {
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let targetStudyInstanceUID;
|
||||
const SeriesInstanceUIDs = [];
|
||||
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
const { SeriesInstanceUID, StudyInstanceUID } = cornerstone.metaData.get(
|
||||
'instance',
|
||||
imageId
|
||||
);
|
||||
|
||||
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
||||
SeriesInstanceUIDs.push(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
if (!targetStudyInstanceUID) {
|
||||
targetStudyInstanceUID = StudyInstanceUID;
|
||||
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
|
||||
console.warn(
|
||||
'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(hydratableMeasurementsInSR).forEach(toolType => {
|
||||
const toolDataForToolType = hydratableMeasurementsInSR[toolType];
|
||||
|
||||
toolDataForToolType.forEach(data => {
|
||||
// Add the measurement to toolState
|
||||
const imageId = sopInstanceUIDToImageId[data.sopInstanceUid];
|
||||
|
||||
data.id = guid();
|
||||
|
||||
_addToolDataToCornerstoneTools(data, toolType, imageId);
|
||||
|
||||
// Let the measurement service know we added to toolState
|
||||
const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
|
||||
toolType,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
imageId
|
||||
);
|
||||
|
||||
const source = MeasurementService.getSource('CornerstoneTools', '4');
|
||||
|
||||
data.label = getLabelFromDCMJSImportedToolData(data);
|
||||
|
||||
MeasurementService.addRawMeasurement(
|
||||
source,
|
||||
toolType,
|
||||
data,
|
||||
toMeasurementSchema
|
||||
);
|
||||
|
||||
if (!imageIds.includes(imageId)) {
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
displaySet.isHydrated = true;
|
||||
|
||||
return {
|
||||
StudyInstanceUID: targetStudyInstanceUID,
|
||||
SeriesInstanceUIDs,
|
||||
};
|
||||
}
|
||||
|
||||
function _addToolDataToCornerstoneTools(data, toolType, imageId) {
|
||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
|
||||
if (toolState[imageId] === undefined) {
|
||||
toolState[imageId] = {};
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[imageId];
|
||||
|
||||
// If we don't have tool state for this type of tool, add an empty object
|
||||
if (imageIdToolState[toolType] === undefined) {
|
||||
imageIdToolState[toolType] = {
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const toolData = imageIdToolState[toolType];
|
||||
|
||||
toolData.data.push(data);
|
||||
}
|
||||
@ -6,6 +6,8 @@ const RESPONSE = {
|
||||
CREATE_REPORT: 1,
|
||||
ADD_SERIES: 2,
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
NO_NOT_FOR_SERIES: 4,
|
||||
HYDRATE_REPORT: 5,
|
||||
};
|
||||
|
||||
const machineConfiguration = {
|
||||
@ -14,6 +16,11 @@ const machineConfiguration = {
|
||||
context: {
|
||||
trackedStudy: '',
|
||||
trackedSeries: [],
|
||||
ignoredSeries: [],
|
||||
//
|
||||
prevTrackedStudy: '',
|
||||
prevTrackedSeries: [],
|
||||
prevIgnoredSeries: [],
|
||||
},
|
||||
states: {
|
||||
off: {
|
||||
@ -29,6 +36,7 @@ const machineConfiguration = {
|
||||
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||
},
|
||||
],
|
||||
PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport',
|
||||
},
|
||||
},
|
||||
promptBeginTracking: {
|
||||
@ -65,7 +73,6 @@ const machineConfiguration = {
|
||||
cond: 'isNewSeries',
|
||||
},
|
||||
],
|
||||
|
||||
UNTRACK_SERIES: [
|
||||
{
|
||||
target: 'tracking',
|
||||
@ -82,6 +89,7 @@ const machineConfiguration = {
|
||||
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||
},
|
||||
],
|
||||
SAVE_REPORT: 'promptSaveReport',
|
||||
},
|
||||
},
|
||||
promptTrackNewSeries: {
|
||||
@ -95,10 +103,16 @@ const machineConfiguration = {
|
||||
},
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: ['setTrackedStudyAndSeries'],
|
||||
actions: [
|
||||
'discardPreviouslyTrackedMeasurements',
|
||||
'setTrackedStudyAndSeries',
|
||||
],
|
||||
cond: 'shouldSetStudyAndSeries',
|
||||
},
|
||||
// CREATE_REPORT && CANCEL
|
||||
{
|
||||
target: 'promptSaveReport',
|
||||
cond: 'shouldPromptSaveReport',
|
||||
},
|
||||
{
|
||||
target: 'tracking',
|
||||
},
|
||||
@ -114,11 +128,80 @@ const machineConfiguration = {
|
||||
onDone: [
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: ['setTrackedStudyAndSeries'],
|
||||
actions: [
|
||||
'discardPreviouslyTrackedMeasurements',
|
||||
'setTrackedStudyAndSeries',
|
||||
],
|
||||
cond: 'shouldSetStudyAndSeries',
|
||||
},
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: ['ignoreSeries'],
|
||||
cond: 'shouldAddIgnoredSeries',
|
||||
},
|
||||
{
|
||||
target: 'promptSaveReport',
|
||||
cond: 'shouldPromptSaveReport',
|
||||
},
|
||||
{
|
||||
target: 'tracking',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
promptSaveReport: {
|
||||
invoke: {
|
||||
src: 'promptSaveReport',
|
||||
onDone: [
|
||||
// "clicked the save button"
|
||||
// - should clear all measurements
|
||||
// - show DICOM SR
|
||||
{
|
||||
target: 'idle',
|
||||
actions: [
|
||||
'clearAllMeasurements',
|
||||
'showStructuredReportDisplaySetInActiveViewport',
|
||||
],
|
||||
cond: 'shouldSaveAndContinueWithSameReport',
|
||||
},
|
||||
// "starting a new report"
|
||||
// - remove "just saved" measurements
|
||||
// - start tracking a new study + report
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: [
|
||||
'discardPreviouslyTrackedMeasurements',
|
||||
'setTrackedStudyAndSeries',
|
||||
],
|
||||
cond: 'shouldSaveAndStartNewReport',
|
||||
},
|
||||
// Cancel, back to tracking
|
||||
{
|
||||
target: 'tracking',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
promptHydrateStructuredReport: {
|
||||
invoke: {
|
||||
src: 'promptHydrateStructuredReport',
|
||||
onDone: [
|
||||
{
|
||||
target: 'tracking',
|
||||
actions: [
|
||||
'setTrackedStudyAndMultipleSeries',
|
||||
'showSeriesInActiveViewport',
|
||||
],
|
||||
cond: 'shouldHydrateStructuredReport',
|
||||
},
|
||||
{
|
||||
target: 'idle',
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
@ -143,23 +226,66 @@ const defaultOptions = {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
discardPreviouslyTrackedMeasurements: (ctx, evt) => {
|
||||
console.log('discardPreviouslyTrackedMeasurements: not implemented');
|
||||
},
|
||||
clearAllMeasurements: (ctx, evt) => {
|
||||
console.log('clearAllMeasurements: not implemented');
|
||||
},
|
||||
showSeriesInActiveViewport: (ctx, evt) => {
|
||||
console.warn('showSeriesInActiveViewport: not implemented');
|
||||
},
|
||||
showStructuredReportDisplaySetInActiveViewport: (ctx, evt) => {
|
||||
console.warn(
|
||||
'showStructuredReportDisplaySetInActiveViewport: not implemented'
|
||||
);
|
||||
},
|
||||
clearContext: assign({
|
||||
trackedStudy: '',
|
||||
trackedSeries: [],
|
||||
ignoredSeries: [],
|
||||
prevTrackedStudy: '',
|
||||
prevTrackedSeries: [],
|
||||
prevIgnoredSeries: [],
|
||||
}),
|
||||
// Promise resolves w/ `evt.data.*`
|
||||
setTrackedStudyAndSeries: assign((ctx, evt) => ({
|
||||
prevTrackedStudy: ctx.trackedStudy,
|
||||
prevTrackedSeries: ctx.trackedSeries.slice(),
|
||||
prevIgnoredSeries: ctx.ignoredSeries.slice(),
|
||||
//
|
||||
trackedStudy: evt.data.StudyInstanceUID,
|
||||
trackedSeries: [evt.data.SeriesInstanceUID],
|
||||
ignoredSeries: [],
|
||||
})),
|
||||
setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({
|
||||
trackedStudy: evt.StudyInstanceUID,
|
||||
trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs],
|
||||
setTrackedStudyAndMultipleSeries: assign((ctx, evt) => {
|
||||
const studyInstanceUID =
|
||||
evt.StudyInstanceUID || evt.data.StudyInstanceUID;
|
||||
const seriesInstanceUIDs =
|
||||
evt.SeriesInstanceUIDs || evt.data.SeriesInstanceUIDs;
|
||||
|
||||
return {
|
||||
prevTrackedStudy: ctx.trackedStudy,
|
||||
prevTrackedSeries: ctx.trackedSeries.slice(),
|
||||
prevIgnoredSeries: ctx.ignoredSeries.slice(),
|
||||
//
|
||||
trackedStudy: studyInstanceUID,
|
||||
trackedSeries: [...ctx.trackedSeries, ...seriesInstanceUIDs],
|
||||
ignoredSeries: [],
|
||||
};
|
||||
}),
|
||||
ignoreSeries: assign((ctx, evt) => ({
|
||||
prevIgnoredSeries: [...ctx.ignoredSeries],
|
||||
ignoredSeries: [...ctx.ignoredSeries, evt.data.SeriesInstanceUID],
|
||||
})),
|
||||
addTrackedSeries: assign((ctx, evt) => ({
|
||||
prevTrackedSeries: [...ctx.trackedSeries],
|
||||
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
|
||||
})),
|
||||
removeTrackedSeries: assign((ctx, evt) => ({
|
||||
prevTrackedSeries: ctx.trackedSeries
|
||||
.slice()
|
||||
.filter(ser => ser !== evt.SeriesInstanceUID),
|
||||
trackedSeries: ctx.trackedSeries
|
||||
.slice()
|
||||
.filter(ser => ser !== evt.SeriesInstanceUID),
|
||||
@ -172,13 +298,30 @@ const defaultOptions = {
|
||||
evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES,
|
||||
shouldSetStudyAndSeries: (ctx, evt) =>
|
||||
evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES,
|
||||
shouldAddIgnoredSeries: (ctx, evt) =>
|
||||
evt.data && evt.data.userResponse === RESPONSE.NO_NOT_FOR_SERIES,
|
||||
shouldPromptSaveReport: (ctx, evt) =>
|
||||
evt.data && evt.data.userResponse === RESPONSE.CREATE_REPORT,
|
||||
shouldSaveAndContinueWithSameReport: (ctx, evt) =>
|
||||
evt.data &&
|
||||
evt.data.userResponse === RESPONSE.CREATE_REPORT &&
|
||||
evt.data.isBackupSave === true,
|
||||
shouldSaveAndStartNewReport: (ctx, evt) =>
|
||||
evt.data &&
|
||||
evt.data.userResponse === RESPONSE.CREATE_REPORT &&
|
||||
evt.data.isBackupSave === false,
|
||||
shouldHydrateStructuredReport: (ctx, evt) =>
|
||||
evt.data && evt.data.userResponse === RESPONSE.HYDRATE_REPORT,
|
||||
// Has more than 1, or SeriesInstanceUID is not in list
|
||||
// --> Post removal would have non-empty trackedSeries array
|
||||
hasRemainingTrackedSeries: (ctx, evt) =>
|
||||
ctx.trackedSeries.length > 1 ||
|
||||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
|
||||
isNewStudy: (ctx, evt) => ctx.trackedStudy !== evt.StudyInstanceUID,
|
||||
isNewStudy: (ctx, evt) =>
|
||||
!ctx.ignoredSeries.includes(evt.SeriesInstanceUID) &&
|
||||
ctx.trackedStudy !== evt.StudyInstanceUID,
|
||||
isNewSeries: (ctx, evt) =>
|
||||
!ctx.ignoredSeries.includes(evt.SeriesInstanceUID) &&
|
||||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
|
||||
},
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ function promptUser({ servicesManager }, ctx, evt) {
|
||||
userResponse: promptResult,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
import hydrateStructuredReport from './_hydrateStructuredReport.js';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
CREATE_REPORT: 1,
|
||||
ADD_SERIES: 2,
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
NO_NOT_FOR_SERIES: 4,
|
||||
HYDRATE_REPORT: 5,
|
||||
};
|
||||
|
||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
const { UIViewportDialogService } = servicesManager.services;
|
||||
const { viewportIndex, displaySetInstanceUID } = evt;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
const promptResult = await _askTrackMeasurements(
|
||||
UIViewportDialogService,
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
// Need to do action here... So we can set state...
|
||||
let StudyInstanceUID, SeriesInstanceUIDs;
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_REPORT) {
|
||||
console.warn('!! HYDRATING STRUCTURED REPORT');
|
||||
const hydrationResult = hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
StudyInstanceUID = hydrationResult.StudyInstanceUID;
|
||||
SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs;
|
||||
}
|
||||
|
||||
resolve({
|
||||
userResponse: promptResult,
|
||||
displaySetInstanceUID: evt.displaySetInstanceUID,
|
||||
viewportIndex,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUIDs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const message =
|
||||
'Do you want to continue tracking measurements for this study?';
|
||||
const actions = [
|
||||
{
|
||||
type: 'secondary',
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_REPORT,
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
UIViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
UIViewportDialogService.show({
|
||||
viewportIndex,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
UIViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default promptUser;
|
||||
@ -0,0 +1,62 @@
|
||||
import createReportAsync from './../../_shared/createReportAsync.js';
|
||||
import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
|
||||
import RESPONSE from '../../_shared/PROMPT_RESPONSES';
|
||||
|
||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||
const viewportIndex =
|
||||
evt.viewportIndex === undefined
|
||||
? evt.data.viewportIndex
|
||||
: evt.viewportIndex;
|
||||
const isBackupSave =
|
||||
evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
|
||||
const StudyInstanceUID = evt?.data?.StudyInstanceUID;
|
||||
const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
|
||||
|
||||
const { trackedStudy, trackedSeries } = ctx;
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
// TODO: Fallback if (UIDialogService) {
|
||||
const promptResult = await createReportDialogPrompt(UIDialogService);
|
||||
|
||||
if (promptResult.action === RESPONSE.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedStudy === m.referenceStudyUID &&
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
const SeriesDescription =
|
||||
// isUndefinedOrEmpty
|
||||
promptResult.value === undefined || promptResult.value === ''
|
||||
? 'Research Derived Series' // default
|
||||
: promptResult.value; // provided value
|
||||
|
||||
displaySetInstanceUIDs = await createReportAsync(
|
||||
servicesManager,
|
||||
dataSource,
|
||||
trackedMeasurements,
|
||||
{
|
||||
SeriesDescription,
|
||||
}
|
||||
);
|
||||
} else if (promptResult.action === RESPONSE.CANCEL) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
resolve({
|
||||
userResponse: promptResult.action,
|
||||
createdDisplaySetInstanceUIDs: displaySetInstanceUIDs,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
isBackupSave,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default promptUser;
|
||||
@ -1,20 +1,15 @@
|
||||
import createReportAsync from './../../_shared/createReportAsync.js';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
CREATE_REPORT: 1,
|
||||
ADD_SERIES: 2,
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
NO_NOT_FOR_SERIES: 4,
|
||||
};
|
||||
|
||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
const {
|
||||
UIViewportDialogService,
|
||||
MeasurementService,
|
||||
} = servicesManager.services;
|
||||
const { UIViewportDialogService } = servicesManager.services;
|
||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
||||
const { trackedStudy, trackedSeries } = ctx;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
let promptResult = await _askShouldAddMeasurements(
|
||||
@ -29,25 +24,12 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
);
|
||||
}
|
||||
|
||||
if (promptResult === RESPONSE.CREATE_REPORT) {
|
||||
// TODO -> Eventually deal with multiple dataSources.
|
||||
// Would need some way of saying which one is the "push" dataSource
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedStudy === m.referenceStudyUID &&
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
createReportAsync(servicesManager, dataSource, trackedMeasurements);
|
||||
}
|
||||
|
||||
resolve({
|
||||
userResponse: promptResult,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
isBackupSave: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -96,7 +78,7 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
|
||||
{ type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
|
||||
{
|
||||
type: 'secondary',
|
||||
text: 'Save in report',
|
||||
text: 'Save',
|
||||
value: RESPONSE.CREATE_REPORT,
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,22 +1,17 @@
|
||||
import createReportAsync from './../../_shared/createReportAsync.js';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
CREATE_REPORT: 1,
|
||||
ADD_SERIES: 2,
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
NO_NOT_FOR_SERIES: 4,
|
||||
};
|
||||
|
||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
const {
|
||||
UIViewportDialogService,
|
||||
MeasurementService,
|
||||
} = servicesManager.services;
|
||||
const { UIViewportDialogService } = servicesManager.services;
|
||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
||||
const { trackedStudy, trackedSeries } = ctx;
|
||||
|
||||
return new Promise(async function (resolve, reject) {
|
||||
return new Promise(async function(resolve, reject) {
|
||||
let promptResult = await _askTrackMeasurements(
|
||||
UIViewportDialogService,
|
||||
viewportIndex
|
||||
@ -29,34 +24,26 @@ function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||
);
|
||||
}
|
||||
|
||||
if (promptResult === RESPONSE.CREATE_REPORT) {
|
||||
// TODO -> Eventually deal with multiple dataSources.
|
||||
// Would need some way of saying which one is the "push" dataSource
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedStudy === m.referenceStudyUID &&
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
createReportAsync(servicesManager, dataSource, trackedMeasurements);
|
||||
}
|
||||
|
||||
resolve({
|
||||
userResponse: promptResult,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
viewportIndex,
|
||||
isBackupSave: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Track measurements for this series?';
|
||||
return new Promise(function(resolve, reject) {
|
||||
const message = 'Track measurements for this seies?';
|
||||
const actions = [
|
||||
{ type: 'cancel', text: 'No', value: RESPONSE.CANCEL },
|
||||
{
|
||||
type: 'secondary',
|
||||
text: 'No, do not ask again for this series',
|
||||
value: RESPONSE.NO_NOT_FOR_SERIES,
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
text: 'Yes',
|
||||
@ -83,7 +70,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||
}
|
||||
|
||||
function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const message =
|
||||
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?';
|
||||
const actions = [
|
||||
|
||||
@ -11,8 +11,8 @@ import { DicomMetadataStore, DICOMSR, utils } from '@ohif/core';
|
||||
import { useDebounce } from '@hooks';
|
||||
import ActionButtons from './ActionButtons';
|
||||
import { useTrackedMeasurements } from '../../getContextModule';
|
||||
import createReportAsync from './../../_shared/createReportAsync.js';
|
||||
import setCornerstoneMeasurementActive from '../../_shared/setCornerstoneMeasurementActive';
|
||||
import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
|
||||
import RESPONSES from '../../_shared/PROMPT_RESPONSES';
|
||||
|
||||
const { formatDate } = utils;
|
||||
|
||||
@ -32,11 +32,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
measurementChangeTimestamp,
|
||||
200
|
||||
);
|
||||
const {
|
||||
MeasurementService,
|
||||
UIDialogService,
|
||||
DisplaySetService,
|
||||
} = servicesManager.services;
|
||||
const { MeasurementService, UIDialogService } = servicesManager.services;
|
||||
const [
|
||||
trackedMeasurements,
|
||||
sendTrackedMeasurementsEvent,
|
||||
@ -132,11 +128,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
};
|
||||
}, [MeasurementService, sendTrackedMeasurementsEvent]);
|
||||
|
||||
function createReport() {
|
||||
// TODO -> Eventually deal with multiple dataSources.
|
||||
// Would need some way of saying which one is the "push" dataSource
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
async function exportReport() {
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
@ -144,20 +136,21 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
|
||||
return createReportAsync(servicesManager, dataSource, trackedMeasurements);
|
||||
}
|
||||
const promptResult = await createReportDialogPrompt(UIDialogService);
|
||||
|
||||
function exportReport() {
|
||||
const measurements = MeasurementService.getMeasurements();
|
||||
const trackedMeasurements = measurements.filter(
|
||||
m =>
|
||||
trackedStudy === m.referenceStudyUID &&
|
||||
trackedSeries.includes(m.referenceSeriesUID)
|
||||
);
|
||||
if (promptResult.action === RESPONSES.CREATE_REPORT) {
|
||||
const additionalFindings = ['ArrowAnnotate'];
|
||||
|
||||
const additionalFindings = ['ArrowAnnotate'];
|
||||
const SeriesDescription =
|
||||
// isUndefinedOrEmpty
|
||||
promptResult.value === undefined || promptResult.value === ''
|
||||
? 'Research Derived Series' // default
|
||||
: promptResult.value; // provided value
|
||||
|
||||
DICOMSR.downloadReport(trackedMeasurements, additionalFindings);
|
||||
DICOMSR.downloadReport(trackedMeasurements, additionalFindings, {
|
||||
SeriesDescription,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const jumpToImage = ({ id, isActive }) => {
|
||||
@ -273,7 +266,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
<div className="flex justify-center p-4">
|
||||
<ActionButtons
|
||||
onExportClick={exportReport}
|
||||
onCreateReportClick={createReport}
|
||||
onCreateReportClick={() => {
|
||||
sendTrackedMeasurementsEvent('SAVE_REPORT', {
|
||||
viewportIndex: viewportGrid.activeViewportIndex,
|
||||
isBackupSave: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -448,7 +448,7 @@ function _createStudyBrowserTabs(
|
||||
ds => ds.StudyInstanceUID === study.studyInstanceUid
|
||||
);
|
||||
|
||||
displaySetsForStudy.sort((a,b) => {
|
||||
displaySetsForStudy.sort((a, b) => {
|
||||
if (a.seriesNumber !== b.seriesNumber) {
|
||||
return a.seriesNumber - b.seriesNumber;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
stowSRFromMeasurements,
|
||||
} from './handleStructuredReport';
|
||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
@ -47,11 +48,20 @@ const retrieveMeasurements = server => {
|
||||
*
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
* as opposed to Finding Sites.
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
const downloadReport = (measurementData, additionalFindingTypes = []) => {
|
||||
const srDataset = generateReport(measurementData, additionalFindingTypes);
|
||||
const downloadReport = (
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options = {}
|
||||
) => {
|
||||
const srDataset = generateReport(
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options
|
||||
);
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
@ -64,19 +74,29 @@ const downloadReport = (measurementData, additionalFindingTypes = []) => {
|
||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
*
|
||||
*/
|
||||
const generateReport = (measurementData, additionalFindingTypes) => {
|
||||
const generateReport = (
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options = {}
|
||||
) => {
|
||||
const filteredToolState = _getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
);
|
||||
|
||||
const report = MeasurementReport.generateReport(
|
||||
filteredToolState,
|
||||
cornerstone.metaData
|
||||
);
|
||||
|
||||
return report.dataset;
|
||||
const { dataset } = report;
|
||||
|
||||
// Add in top level series options
|
||||
Object.assign(dataset, options);
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -85,13 +105,14 @@ const generateReport = (measurementData, additionalFindingTypes) => {
|
||||
* that you wish to serialize.
|
||||
* @param {object} dataSource The dataSource that you wish to use to persist the data.
|
||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||
* as opposed to Finding Sites.
|
||||
* @param {object} options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
* @return {object} The naturalized report
|
||||
*/
|
||||
const storeMeasurements = async (
|
||||
measurementData,
|
||||
dataSource,
|
||||
additionalFindingTypes = []
|
||||
additionalFindingTypes,
|
||||
options = {}
|
||||
) => {
|
||||
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
|
||||
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||
@ -105,7 +126,8 @@ const storeMeasurements = async (
|
||||
try {
|
||||
const naturalizedReport = generateReport(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
additionalFindingTypes,
|
||||
options
|
||||
);
|
||||
const { StudyInstanceUID } = naturalizedReport;
|
||||
|
||||
@ -117,6 +139,7 @@ const storeMeasurements = async (
|
||||
|
||||
return naturalizedReport;
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
log.error(
|
||||
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||
);
|
||||
@ -124,6 +147,8 @@ const storeMeasurements = async (
|
||||
}
|
||||
};
|
||||
|
||||
// _getFilteredCornerstoneToolState
|
||||
// DIFFERENT IMPLEMENTATION HERE! What's up?
|
||||
function _getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
@ -147,7 +172,6 @@ function _getFilteredCornerstoneToolState(
|
||||
}
|
||||
|
||||
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
|
||||
|
||||
const toolData = imageIdSpecificToolState[toolType].data;
|
||||
|
||||
let finding;
|
||||
|
||||
@ -16,7 +16,7 @@ export default class ExtensionManager {
|
||||
this.moduleTypeNames.forEach(moduleType => {
|
||||
this.modules[moduleType] = [];
|
||||
});
|
||||
this._extensionLifeCycleHooks = { onModeEnter: {} };
|
||||
this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
|
||||
this.dataSourceMap = {};
|
||||
this.defaultDataSourceName = appConfig.defaultDataSourceName;
|
||||
this.activeDataSource = undefined;
|
||||
@ -29,12 +29,21 @@ export default class ExtensionManager {
|
||||
onModeEnter() {
|
||||
const {
|
||||
registeredExtensionIds,
|
||||
getModuleEntry,
|
||||
_servicesManager,
|
||||
_commandsManager,
|
||||
_extensionLifeCycleHooks,
|
||||
} = this;
|
||||
|
||||
const {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
} = _servicesManager.services;
|
||||
|
||||
MeasurementService.clearMeasurements();
|
||||
ViewportGridService.reset();
|
||||
HangingProtocolService.reset();
|
||||
|
||||
registeredExtensionIds.forEach(extensionId => {
|
||||
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId];
|
||||
|
||||
@ -47,6 +56,36 @@ export default class ExtensionManager {
|
||||
});
|
||||
}
|
||||
|
||||
onModeExit() {
|
||||
const {
|
||||
registeredExtensionIds,
|
||||
_servicesManager,
|
||||
_commandsManager,
|
||||
_extensionLifeCycleHooks,
|
||||
} = this;
|
||||
|
||||
const {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
} = _servicesManager.services;
|
||||
|
||||
MeasurementService.clearMeasurements();
|
||||
ViewportGridService.reset();
|
||||
HangingProtocolService.reset();
|
||||
|
||||
registeredExtensionIds.forEach(extensionId => {
|
||||
const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId];
|
||||
|
||||
if (typeof onModeExit === 'function') {
|
||||
onModeExit({
|
||||
servicesManager: _servicesManager,
|
||||
commandsManager: _commandsManager,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of extensions, or an array of arrays that contains extension
|
||||
* configuration pairs.
|
||||
@ -110,6 +149,11 @@ export default class ExtensionManager {
|
||||
extension.onModeEnter;
|
||||
}
|
||||
|
||||
if (extension.onModeExit) {
|
||||
this._extensionLifeCycleHooks.onModeExit[extensionId] =
|
||||
extension.onModeExit;
|
||||
}
|
||||
|
||||
// Register Modules
|
||||
this.moduleTypeNames.forEach(moduleType => {
|
||||
const extensionModule = this._getExtensionModule(
|
||||
|
||||
@ -77,10 +77,10 @@ class ConformanceCriteria {
|
||||
this.nonconformities = nonconformities;
|
||||
this.groupedNonConformities = groupedNonConformities;
|
||||
|
||||
console.warn('nonconformities');
|
||||
console.warn(nonconformities);
|
||||
console.warn('groupedNonConformities');
|
||||
console.warn(groupedNonConformities);
|
||||
// console.warn('nonconformities');
|
||||
// console.warn(nonconformities);
|
||||
// console.warn('groupedNonConformities');
|
||||
// console.warn(groupedNonConformities);
|
||||
|
||||
return nonconformities;
|
||||
}
|
||||
|
||||
@ -57,9 +57,7 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
const BaseImplementation = {
|
||||
EVENTS,
|
||||
listeners: {},
|
||||
// TODO: The assumption is that this is called per Study per Series
|
||||
// We should do more to verify/clarify that
|
||||
addInstances(instances) {
|
||||
addInstances(instances, madeInClient = false) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
|
||||
|
||||
let study = _model.studies.find(
|
||||
@ -81,6 +79,7 @@ const BaseImplementation = {
|
||||
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
madeInClient,
|
||||
});
|
||||
},
|
||||
addSeriesMetadata(seriesSummaryMetadata) {
|
||||
|
||||
@ -36,6 +36,10 @@ export default class DisplaySetService {
|
||||
return displaySetCache;
|
||||
}
|
||||
|
||||
getMostRecentDisplaySet() {
|
||||
return this.activeDisplaySets[this.activeDisplaySets.length - 1];
|
||||
}
|
||||
|
||||
getActiveDisplaySets() {
|
||||
return this.activeDisplaySets;
|
||||
}
|
||||
@ -69,6 +73,12 @@ export default class DisplaySetService {
|
||||
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUid
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} input
|
||||
* @param {*} param1
|
||||
* @returns {string[]} - added displaySetInstanceUIDs
|
||||
*/
|
||||
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
|
||||
if (!input || !input.length) {
|
||||
throw new Error('No instances were provided.');
|
||||
@ -84,11 +94,12 @@ export default class DisplaySetService {
|
||||
let displaySetsAdded = [];
|
||||
|
||||
if (batch) {
|
||||
input.forEach(instances => {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const instances = input[i];
|
||||
const displaySets = this.makeDisplaySetForInstances(instances);
|
||||
|
||||
displaySetsAdded = [...displaySetsAdded, displaySets];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const displaySets = this.makeDisplaySetForInstances(input);
|
||||
|
||||
@ -109,6 +120,8 @@ export default class DisplaySetService {
|
||||
displaySetsAdded,
|
||||
options,
|
||||
});
|
||||
|
||||
return displaySetsAdded;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ const ViewportActionBar = ({
|
||||
showNavArrows,
|
||||
showPatientInfo: patientInfoVisibility,
|
||||
onSeriesChange,
|
||||
onHydrationClick,
|
||||
onDoubleClick,
|
||||
}) => {
|
||||
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||
@ -32,7 +31,6 @@ const ViewportActionBar = ({
|
||||
label,
|
||||
isTracked,
|
||||
isLocked,
|
||||
isHydrated,
|
||||
modality,
|
||||
studyDate,
|
||||
currentSeries,
|
||||
@ -78,13 +76,7 @@ const ViewportActionBar = ({
|
||||
Measurements cannot be duplicated.
|
||||
</div>
|
||||
)
|
||||
: () => (
|
||||
<div>
|
||||
This SR is unlocked. <br />
|
||||
You can duplicate measurements on your current report <br /> by
|
||||
clicking 'Edit'.
|
||||
</div>
|
||||
);
|
||||
: () => <div>This SR is unlocked.</div>;
|
||||
return (
|
||||
<>
|
||||
<Tooltip content={<TooltipMessage />} position="bottom-left">
|
||||
@ -101,16 +93,6 @@ const ViewportActionBar = ({
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!isLocked && !isHydrated && (
|
||||
<div className="relative flex p-1 ml-1 border rounded cursor-pointer border-primary-light">
|
||||
<span
|
||||
className="text-sm font-bold leading-none text-primary-light"
|
||||
onClick={onHydrationClick}
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -104,14 +104,15 @@ export default function ModeRoute({
|
||||
// TODO: For some reason this is running before the Providers
|
||||
// are calling setServiceImplementation
|
||||
// TOOD -> iterate through services.
|
||||
MeasurementService.clearMeasurements();
|
||||
ViewportGridService.reset();
|
||||
HangingProtocolService.reset();
|
||||
|
||||
// Extension
|
||||
extensionManager.onModeEnter();
|
||||
// Mode
|
||||
route.init({ servicesManager, extensionManager });
|
||||
|
||||
return () => {
|
||||
extensionManager.onModeExit();
|
||||
};
|
||||
}, [
|
||||
mode,
|
||||
dataSourceName,
|
||||
@ -131,13 +132,16 @@ export default function ModeRoute({
|
||||
// DisplaySetService would wire this up?
|
||||
const { unsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
({ StudyInstanceUID, SeriesInstanceUID }) => {
|
||||
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
DisplaySetService.makeDisplaySets(seriesMetadata.instances);
|
||||
DisplaySetService.makeDisplaySets(
|
||||
seriesMetadata.instances,
|
||||
madeInClient
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@ -161,9 +165,7 @@ export default function ModeRoute({
|
||||
const { unsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||
({ StudyInstanceUID }) => {
|
||||
const studyMetadata = DicomMetadataStore.getStudy(
|
||||
StudyInstanceUID,
|
||||
);
|
||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
|
||||
const sortedSeries = studyMetadata.series.sort((a, b) => {
|
||||
const aLowPriority = isLowPriorityModality(a.Modality);
|
||||
@ -176,13 +178,12 @@ export default function ModeRoute({
|
||||
}
|
||||
|
||||
return a.SeriesNumber - b.SeriesNumber;
|
||||
})
|
||||
});
|
||||
|
||||
const { SeriesInstanceUID } = sortedSeries[0]
|
||||
const { SeriesInstanceUID } = sortedSeries[0];
|
||||
|
||||
HangingProtocolService.setHangingProtocol(
|
||||
{
|
||||
/*protocolMatchingRules: [
|
||||
HangingProtocolService.setHangingProtocol({
|
||||
/*protocolMatchingRules: [
|
||||
{
|
||||
id: '7tmuq7KzDMCWFeapc',
|
||||
weight: 2,
|
||||
@ -195,9 +196,9 @@ export default function ModeRoute({
|
||||
},
|
||||
},
|
||||
],*/
|
||||
stages: [
|
||||
{
|
||||
/*id: 'v5PfGt9F6mffZPif5',
|
||||
stages: [
|
||||
{
|
||||
/*id: 'v5PfGt9F6mffZPif5',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
@ -206,30 +207,29 @@ export default function ModeRoute({
|
||||
},
|
||||
layoutTemplateName: 'gridLayout',
|
||||
},*/
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: {},
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'mXnsCcNzZL56z7mTZ',
|
||||
weight: 1,
|
||||
required: true,
|
||||
attribute: 'SeriesInstanceUID',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: SeriesInstanceUID,
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: {},
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'mXnsCcNzZL56z7mTZ',
|
||||
weight: 1,
|
||||
required: true,
|
||||
attribute: 'SeriesInstanceUID',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: SeriesInstanceUID,
|
||||
},
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
return unsubscribe;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user