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

* workflow changes

* Cherry pick OHIF-310 into this PR.

* fix label bug and dead code

* remove comment

* Wire up CreateReportButton

* fix additionalFindingTypes + save interaction

* Remove unused createReportAsync call

* stub for report hydration prompt

* Update PR

* Remove debuggers

* push updated code

* Push updates

* push

* fix sidebar save

* enable tracking of new study or series

* fix for duplicate created report series in sidebard

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

* Fix for hydration not marking multiple series as active

* Reviewer changes.

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

Co-authored-by: James A. Petts <jamesapetts@gmail.com>
This commit is contained in:
Danny Brown 2020-08-04 05:11:23 -04:00 committed by GitHub
parent 5f487840a1
commit 2a51d75f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 948 additions and 391 deletions

View File

@ -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,

View File

@ -334,6 +334,12 @@ const _connectToolsToMeasurementService = (
}
}
/**
* When csTools fires a removed event, remove the same measurement
* from the measurement service
*
* @param {*} csToolsEvent
*/
function removeMeasurement(csToolsEvent) {
console.log('~~ removeEvt', csToolsEvent);
try {
@ -381,15 +387,42 @@ const _connectMeasurementServiceToTools = (
});
});
/* TODO: Remove per measurement
MeasurementService.subscribe(MEASUREMENT_REMOVED,
({ source, measurement }) => {
if ([sourceId].includes(source.id)) {
// const annotation = getAnnotation('Length', measurement.id);
// iterate tool state
}
// TODO: This is an unsafe delete
// Cornerstone-tools should probably expose a more generic "delete by id"
// And have toolState managers expose a method to find any of their toolState by ID
// --> csTools.deleteById --> internally checks all registered modules/managers?
//
// This implementation assumes a single globalImageIdSpecificToolStateManager
// It iterates all toolState for all toolTypes, and deletes any with a matching id
//
// Could potentially use "source" from event to determine tool type and skip some
// iterations?
MeasurementService.subscribe(
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {
// THIS POINTS TO ORIGINAL; Not a copy
const imageIdSpecificToolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
// ImageId -->
Object.keys(imageIdSpecificToolState).forEach(imageId => {
// ImageId --> Tool -->
Object.keys(imageIdSpecificToolState[imageId]).forEach(toolName => {
const toolState = imageIdSpecificToolState[imageId][toolName];
let annotationIndex = toolState.data.length - 1;
while (annotationIndex >= 0) {
const annotation = toolState.data[annotationIndex];
if (annotation.id === removedMeasurementId) {
toolState.data.splice(annotationIndex, 1);
}
annotationIndex--;
}
});
});
}
); */
);
};
// const {

View File

@ -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'

View File

@ -14,7 +14,7 @@ const defaultConfig = {
/**
* @param {object} configuration
*/
export default function init({ configuration = {}, servicesManager }) {
export default function init({ configuration = {} }) {
const conifg = Object.assign({}, defaultConfig, configuration);
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
@ -22,22 +22,4 @@ export default function init({ configuration = {}, servicesManager }) {
cornerstoneTools.register('module', id, dicomSRModule);
cornerstoneTools.addTool(DICOMSRDisplayTool);
cornerstoneTools.setToolEnabled(TOOL_NAMES.DICOM_SR_DISPLAY_TOOL);
const { DisplaySetService } = servicesManager.services;
DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
data => {
const { displaySetsAdded, options } = data;
displaySetsAdded.forEach(dSet => {
if (options.madeInClient) {
// Set just made displaySets as hydrated.
if (!dSet.isLoaded) {
dSet.load();
}
dSet.isHydrated = true;
}
});
}
);
}

View File

@ -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;

View File

@ -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',

View File

@ -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>
);
},
},
});
});
}

View File

@ -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]}

View File

@ -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);
}

View File

@ -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),
},
};

View File

@ -20,6 +20,7 @@ function promptUser({ servicesManager }, ctx, evt) {
userResponse: promptResult,
StudyInstanceUID,
SeriesInstanceUID,
viewportIndex,
});
});
}

View File

@ -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;

View File

@ -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;

View File

@ -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,
},
{

View File

@ -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 = [

View File

@ -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>
</>

View File

@ -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;
}

View File

@ -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;

View File

@ -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(

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
}
};

View File

@ -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 &apos;Edit&apos;.
</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>
)}
</>
);
}

View File

@ -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;