[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": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
"version": "detect"
|
"version": "detect"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"react/jsx-props-no-spreading": "error"
|
"react/jsx-props-no-spreading": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "false"
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"cy": true,
|
"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) {
|
function removeMeasurement(csToolsEvent) {
|
||||||
console.log('~~ removeEvt', csToolsEvent);
|
console.log('~~ removeEvt', csToolsEvent);
|
||||||
try {
|
try {
|
||||||
@ -381,15 +387,42 @@ const _connectMeasurementServiceToTools = (
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* TODO: Remove per measurement
|
// TODO: This is an unsafe delete
|
||||||
MeasurementService.subscribe(MEASUREMENT_REMOVED,
|
// Cornerstone-tools should probably expose a more generic "delete by id"
|
||||||
({ source, measurement }) => {
|
// And have toolState managers expose a method to find any of their toolState by ID
|
||||||
if ([sourceId].includes(source.id)) {
|
// --> csTools.deleteById --> internally checks all registered modules/managers?
|
||||||
// const annotation = getAnnotation('Length', measurement.id);
|
//
|
||||||
// iterate tool state
|
// 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 {
|
// const {
|
||||||
|
|||||||
@ -4,11 +4,15 @@ import cornerstoneTools from 'cornerstone-tools';
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||||
import OHIF, { DicomMetadataStore, utils } from '@ohif/core';
|
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 TOOL_NAMES from './constants/toolNames';
|
||||||
import { adapters } from 'dcmjs';
|
import { adapters } from 'dcmjs';
|
||||||
import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
|
// import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema';
|
||||||
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
|
||||||
import id from './id';
|
import id from './id';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
@ -30,6 +34,7 @@ function OHIFCornerstoneSRViewport({
|
|||||||
}) {
|
}) {
|
||||||
const { DisplaySetService, MeasurementService } = servicesManager.services;
|
const { DisplaySetService, MeasurementService } = servicesManager.services;
|
||||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||||
const [measurementCount, setMeasurementCount] = useState(1);
|
const [measurementCount, setMeasurementCount] = useState(1);
|
||||||
const [viewportData, setViewportData] = useState(null);
|
const [viewportData, setViewportData] = useState(null);
|
||||||
@ -141,14 +146,9 @@ function OHIFCornerstoneSRViewport({
|
|||||||
const numMeasurements = displaySet.measurements.length;
|
const numMeasurements = displaySet.measurements.length;
|
||||||
|
|
||||||
setMeasurementCount(numMeasurements);
|
setMeasurementCount(numMeasurements);
|
||||||
}, [
|
}, [dataSource, displaySet]);
|
||||||
dataSource,
|
|
||||||
displaySet,
|
|
||||||
displaySet.StudyInstanceUID,
|
|
||||||
displaySet.displaySetInstanceUID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const updateViewport = newMeasurementSelected => {
|
const updateViewport = useCallback(newMeasurementSelected => {
|
||||||
const {
|
const {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
@ -181,28 +181,25 @@ function OHIFCornerstoneSRViewport({
|
|||||||
cornerstone.updateImage(element);
|
cornerstone.updateImage(element);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
if (element !== null) {
|
() => {
|
||||||
setTrackingUniqueIdentifiersForElement(element);
|
if (element !== null) {
|
||||||
}
|
setTrackingUniqueIdentifiersForElement(element);
|
||||||
}, [
|
}
|
||||||
dataSource,
|
},
|
||||||
displaySet,
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
displaySet.StudyInstanceUID,
|
[dataSource, displaySet]
|
||||||
displaySet.displaySetInstanceUID,
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
updateViewport(measurementSelected);
|
() => {
|
||||||
}, [
|
updateViewport(measurementSelected);
|
||||||
dataSource,
|
},
|
||||||
displaySet,
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
displaySet.StudyInstanceUID,
|
[dataSource, displaySet, element]
|
||||||
displaySet.displaySetInstanceUID,
|
);
|
||||||
element,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex(
|
const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex(
|
||||||
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
|
vp => vp.displaySetInstanceUID === displaySet.displaySetInstanceUID
|
||||||
@ -273,147 +270,6 @@ function OHIFCornerstoneSRViewport({
|
|||||||
updateViewport(newMeasurementSelected);
|
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] : '';
|
const label = viewports.length > 1 ? _viewportLabels[viewportIndex] : '';
|
||||||
|
|
||||||
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
|
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
|
||||||
@ -426,7 +282,6 @@ function OHIFCornerstoneSRViewport({
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}}
|
}}
|
||||||
onSeriesChange={onMeasurementChange}
|
onSeriesChange={onMeasurementChange}
|
||||||
onHydrationClick={hydrateMeasurementService}
|
|
||||||
studyData={{
|
studyData={{
|
||||||
label,
|
label,
|
||||||
isTracked: false,
|
isTracked: false,
|
||||||
@ -447,8 +302,8 @@ function OHIFCornerstoneSRViewport({
|
|||||||
spacing:
|
spacing:
|
||||||
PixelSpacing && PixelSpacing.length
|
PixelSpacing && PixelSpacing.length
|
||||||
? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(
|
? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(
|
||||||
2
|
2
|
||||||
)}mm`
|
)}mm`
|
||||||
: '',
|
: '',
|
||||||
scanner: ManufacturerModelName || '',
|
scanner: ManufacturerModelName || '',
|
||||||
},
|
},
|
||||||
@ -471,6 +326,17 @@ function OHIFCornerstoneSRViewport({
|
|||||||
// seizure inducing strobe blinking effect
|
// seizure inducing strobe blinking effect
|
||||||
resizeRefreshRateMs={150}
|
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}
|
{childrenWithProps}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -582,27 +448,6 @@ async function _getViewportAndActiveDisplaySetData(
|
|||||||
return { viewportData, activeDisplaySetData };
|
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() {
|
function _onDoubleClick() {
|
||||||
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||||
'manipulatorState'
|
'manipulatorState'
|
||||||
|
|||||||
@ -14,7 +14,7 @@ const defaultConfig = {
|
|||||||
/**
|
/**
|
||||||
* @param {object} configuration
|
* @param {object} configuration
|
||||||
*/
|
*/
|
||||||
export default function init({ configuration = {}, servicesManager }) {
|
export default function init({ configuration = {} }) {
|
||||||
const conifg = Object.assign({}, defaultConfig, configuration);
|
const conifg = Object.assign({}, defaultConfig, configuration);
|
||||||
|
|
||||||
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
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.register('module', id, dicomSRModule);
|
||||||
cornerstoneTools.addTool(DICOMSRDisplayTool);
|
cornerstoneTools.addTool(DICOMSRDisplayTool);
|
||||||
cornerstoneTools.setToolEnabled(TOOL_NAMES.DICOM_SR_DISPLAY_TOOL);
|
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 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 {
|
const {
|
||||||
|
DisplaySetService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
DisplaySetService,
|
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
const loadingDialogId = UIDialogService.create({
|
const loadingDialogId = UIDialogService.create({
|
||||||
showOverlay: true,
|
showOverlay: true,
|
||||||
@ -19,17 +32,24 @@ async function createReportAsync(servicesManager, dataSource, measurements) {
|
|||||||
const naturalizedReport = await DICOMSR.storeMeasurements(
|
const naturalizedReport = await DICOMSR.storeMeasurements(
|
||||||
measurements,
|
measurements,
|
||||||
dataSource,
|
dataSource,
|
||||||
['ArrowAnnotate']
|
['ArrowAnnotate'],
|
||||||
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
DisplaySetService.makeDisplaySets([naturalizedReport], {
|
// The "Mode" route listens for DicomMetadataStore changes
|
||||||
madeInClient: true,
|
// When a new instance is added, it listens and
|
||||||
});
|
// automatically calls makeDisplaySets
|
||||||
|
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||||
|
|
||||||
|
const displaySetInstanceUID = DisplaySetService.getMostRecentDisplaySet();
|
||||||
|
|
||||||
UINotificationService.show({
|
UINotificationService.show({
|
||||||
title: 'Create Report',
|
title: 'Create Report',
|
||||||
message: 'Measurements saved successfully',
|
message: 'Measurements saved successfully',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return [displaySetInstanceUID];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
UINotificationService.show({
|
UINotificationService.show({
|
||||||
title: 'Create Report',
|
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 PropTypes from 'prop-types';
|
||||||
import { Machine } from 'xstate';
|
import { Machine } from 'xstate';
|
||||||
import { useMachine } from '@xstate/react';
|
import { useMachine } from '@xstate/react';
|
||||||
|
import { useViewportGrid } from '@ohif/ui';
|
||||||
import {
|
import {
|
||||||
machineConfiguration,
|
machineConfiguration,
|
||||||
defaultOptions,
|
defaultOptions,
|
||||||
@ -9,6 +10,9 @@ import {
|
|||||||
import promptBeginTracking from './promptBeginTracking';
|
import promptBeginTracking from './promptBeginTracking';
|
||||||
import promptTrackNewSeries from './promptTrackNewSeries';
|
import promptTrackNewSeries from './promptTrackNewSeries';
|
||||||
import promptTrackNewStudy from './promptTrackNewStudy';
|
import promptTrackNewStudy from './promptTrackNewStudy';
|
||||||
|
import promptSaveReport from './promptSaveReport';
|
||||||
|
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
||||||
|
import hydrateStructuredReport from './_hydrateStructuredReport.js';
|
||||||
|
|
||||||
const TrackedMeasurementsContext = React.createContext();
|
const TrackedMeasurementsContext = React.createContext();
|
||||||
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
||||||
@ -22,7 +26,60 @@ function TrackedMeasurementsContextProvider(
|
|||||||
{ servicesManager, extensionManager }, // Bound by consumer
|
{ servicesManager, extensionManager }, // Bound by consumer
|
||||||
{ children } // Component props
|
{ children } // Component props
|
||||||
) {
|
) {
|
||||||
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
const { activeViewportIndex, viewports } = viewportGrid;
|
||||||
|
|
||||||
const machineOptions = Object.assign({}, defaultOptions);
|
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, {
|
machineOptions.services = Object.assign({}, machineOptions.services, {
|
||||||
promptBeginTracking: promptBeginTracking.bind(null, {
|
promptBeginTracking: promptBeginTracking.bind(null, {
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -36,6 +93,13 @@ function TrackedMeasurementsContextProvider(
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
}),
|
}),
|
||||||
|
promptSaveReport: promptSaveReport.bind(null, {
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
}),
|
||||||
|
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
||||||
|
servicesManager,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: IMPROVE
|
// TODO: IMPROVE
|
||||||
@ -58,6 +122,41 @@ function TrackedMeasurementsContextProvider(
|
|||||||
trackedMeasurementsService,
|
trackedMeasurementsService,
|
||||||
] = useMachine(measurementTrackingMachine);
|
] = 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 (
|
return (
|
||||||
<TrackedMeasurementsContext.Provider
|
<TrackedMeasurementsContext.Provider
|
||||||
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
|
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,
|
CREATE_REPORT: 1,
|
||||||
ADD_SERIES: 2,
|
ADD_SERIES: 2,
|
||||||
SET_STUDY_AND_SERIES: 3,
|
SET_STUDY_AND_SERIES: 3,
|
||||||
|
NO_NOT_FOR_SERIES: 4,
|
||||||
|
HYDRATE_REPORT: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const machineConfiguration = {
|
const machineConfiguration = {
|
||||||
@ -14,6 +16,11 @@ const machineConfiguration = {
|
|||||||
context: {
|
context: {
|
||||||
trackedStudy: '',
|
trackedStudy: '',
|
||||||
trackedSeries: [],
|
trackedSeries: [],
|
||||||
|
ignoredSeries: [],
|
||||||
|
//
|
||||||
|
prevTrackedStudy: '',
|
||||||
|
prevTrackedSeries: [],
|
||||||
|
prevIgnoredSeries: [],
|
||||||
},
|
},
|
||||||
states: {
|
states: {
|
||||||
off: {
|
off: {
|
||||||
@ -29,6 +36,7 @@ const machineConfiguration = {
|
|||||||
actions: ['setTrackedStudyAndMultipleSeries'],
|
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
PROMPT_HYDRATE_SR: 'promptHydrateStructuredReport',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
promptBeginTracking: {
|
promptBeginTracking: {
|
||||||
@ -65,7 +73,6 @@ const machineConfiguration = {
|
|||||||
cond: 'isNewSeries',
|
cond: 'isNewSeries',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
UNTRACK_SERIES: [
|
UNTRACK_SERIES: [
|
||||||
{
|
{
|
||||||
target: 'tracking',
|
target: 'tracking',
|
||||||
@ -82,6 +89,7 @@ const machineConfiguration = {
|
|||||||
actions: ['setTrackedStudyAndMultipleSeries'],
|
actions: ['setTrackedStudyAndMultipleSeries'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
SAVE_REPORT: 'promptSaveReport',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
promptTrackNewSeries: {
|
promptTrackNewSeries: {
|
||||||
@ -95,10 +103,16 @@ const machineConfiguration = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 'tracking',
|
target: 'tracking',
|
||||||
actions: ['setTrackedStudyAndSeries'],
|
actions: [
|
||||||
|
'discardPreviouslyTrackedMeasurements',
|
||||||
|
'setTrackedStudyAndSeries',
|
||||||
|
],
|
||||||
cond: 'shouldSetStudyAndSeries',
|
cond: 'shouldSetStudyAndSeries',
|
||||||
},
|
},
|
||||||
// CREATE_REPORT && CANCEL
|
{
|
||||||
|
target: 'promptSaveReport',
|
||||||
|
cond: 'shouldPromptSaveReport',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
target: 'tracking',
|
target: 'tracking',
|
||||||
},
|
},
|
||||||
@ -114,11 +128,80 @@ const machineConfiguration = {
|
|||||||
onDone: [
|
onDone: [
|
||||||
{
|
{
|
||||||
target: 'tracking',
|
target: 'tracking',
|
||||||
actions: ['setTrackedStudyAndSeries'],
|
actions: [
|
||||||
|
'discardPreviouslyTrackedMeasurements',
|
||||||
|
'setTrackedStudyAndSeries',
|
||||||
|
],
|
||||||
cond: 'shouldSetStudyAndSeries',
|
cond: 'shouldSetStudyAndSeries',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: 'tracking',
|
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: {
|
onError: {
|
||||||
@ -143,23 +226,66 @@ const defaultOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
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({
|
clearContext: assign({
|
||||||
trackedStudy: '',
|
trackedStudy: '',
|
||||||
trackedSeries: [],
|
trackedSeries: [],
|
||||||
|
ignoredSeries: [],
|
||||||
|
prevTrackedStudy: '',
|
||||||
|
prevTrackedSeries: [],
|
||||||
|
prevIgnoredSeries: [],
|
||||||
}),
|
}),
|
||||||
// Promise resolves w/ `evt.data.*`
|
// Promise resolves w/ `evt.data.*`
|
||||||
setTrackedStudyAndSeries: assign((ctx, evt) => ({
|
setTrackedStudyAndSeries: assign((ctx, evt) => ({
|
||||||
|
prevTrackedStudy: ctx.trackedStudy,
|
||||||
|
prevTrackedSeries: ctx.trackedSeries.slice(),
|
||||||
|
prevIgnoredSeries: ctx.ignoredSeries.slice(),
|
||||||
|
//
|
||||||
trackedStudy: evt.data.StudyInstanceUID,
|
trackedStudy: evt.data.StudyInstanceUID,
|
||||||
trackedSeries: [evt.data.SeriesInstanceUID],
|
trackedSeries: [evt.data.SeriesInstanceUID],
|
||||||
|
ignoredSeries: [],
|
||||||
})),
|
})),
|
||||||
setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({
|
setTrackedStudyAndMultipleSeries: assign((ctx, evt) => {
|
||||||
trackedStudy: evt.StudyInstanceUID,
|
const studyInstanceUID =
|
||||||
trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs],
|
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) => ({
|
addTrackedSeries: assign((ctx, evt) => ({
|
||||||
|
prevTrackedSeries: [...ctx.trackedSeries],
|
||||||
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
|
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
|
||||||
})),
|
})),
|
||||||
removeTrackedSeries: assign((ctx, evt) => ({
|
removeTrackedSeries: assign((ctx, evt) => ({
|
||||||
|
prevTrackedSeries: ctx.trackedSeries
|
||||||
|
.slice()
|
||||||
|
.filter(ser => ser !== evt.SeriesInstanceUID),
|
||||||
trackedSeries: ctx.trackedSeries
|
trackedSeries: ctx.trackedSeries
|
||||||
.slice()
|
.slice()
|
||||||
.filter(ser => ser !== evt.SeriesInstanceUID),
|
.filter(ser => ser !== evt.SeriesInstanceUID),
|
||||||
@ -172,13 +298,30 @@ const defaultOptions = {
|
|||||||
evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES,
|
evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES,
|
||||||
shouldSetStudyAndSeries: (ctx, evt) =>
|
shouldSetStudyAndSeries: (ctx, evt) =>
|
||||||
evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES,
|
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
|
// Has more than 1, or SeriesInstanceUID is not in list
|
||||||
// --> Post removal would have non-empty trackedSeries array
|
// --> Post removal would have non-empty trackedSeries array
|
||||||
hasRemainingTrackedSeries: (ctx, evt) =>
|
hasRemainingTrackedSeries: (ctx, evt) =>
|
||||||
ctx.trackedSeries.length > 1 ||
|
ctx.trackedSeries.length > 1 ||
|
||||||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
|
!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) =>
|
isNewSeries: (ctx, evt) =>
|
||||||
|
!ctx.ignoredSeries.includes(evt.SeriesInstanceUID) &&
|
||||||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
|
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,6 +20,7 @@ function promptUser({ servicesManager }, ctx, evt) {
|
|||||||
userResponse: promptResult,
|
userResponse: promptResult,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
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 = {
|
const RESPONSE = {
|
||||||
NO_NEVER: -1,
|
NO_NEVER: -1,
|
||||||
CANCEL: 0,
|
CANCEL: 0,
|
||||||
CREATE_REPORT: 1,
|
CREATE_REPORT: 1,
|
||||||
ADD_SERIES: 2,
|
ADD_SERIES: 2,
|
||||||
SET_STUDY_AND_SERIES: 3,
|
SET_STUDY_AND_SERIES: 3,
|
||||||
|
NO_NOT_FOR_SERIES: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||||
const {
|
const { UIViewportDialogService } = servicesManager.services;
|
||||||
UIViewportDialogService,
|
|
||||||
MeasurementService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
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 _askShouldAddMeasurements(
|
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({
|
resolve({
|
||||||
userResponse: promptResult,
|
userResponse: promptResult,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
viewportIndex,
|
||||||
|
isBackupSave: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -96,7 +78,7 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
|
|||||||
{ type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
|
{ type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
|
||||||
{
|
{
|
||||||
type: 'secondary',
|
type: 'secondary',
|
||||||
text: 'Save in report',
|
text: 'Save',
|
||||||
value: RESPONSE.CREATE_REPORT,
|
value: RESPONSE.CREATE_REPORT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,22 +1,17 @@
|
|||||||
import createReportAsync from './../../_shared/createReportAsync.js';
|
|
||||||
|
|
||||||
const RESPONSE = {
|
const RESPONSE = {
|
||||||
NO_NEVER: -1,
|
NO_NEVER: -1,
|
||||||
CANCEL: 0,
|
CANCEL: 0,
|
||||||
CREATE_REPORT: 1,
|
CREATE_REPORT: 1,
|
||||||
ADD_SERIES: 2,
|
ADD_SERIES: 2,
|
||||||
SET_STUDY_AND_SERIES: 3,
|
SET_STUDY_AND_SERIES: 3,
|
||||||
|
NO_NOT_FOR_SERIES: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||||
const {
|
const { UIViewportDialogService } = servicesManager.services;
|
||||||
UIViewportDialogService,
|
|
||||||
MeasurementService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
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(
|
let promptResult = await _askTrackMeasurements(
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
viewportIndex
|
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({
|
resolve({
|
||||||
userResponse: promptResult,
|
userResponse: promptResult,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
viewportIndex,
|
||||||
|
isBackupSave: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const message = 'Track measurements for this series?';
|
const message = 'Track measurements for this seies?';
|
||||||
const actions = [
|
const actions = [
|
||||||
{ type: 'cancel', text: 'No', value: RESPONSE.CANCEL },
|
{ 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',
|
type: 'primary',
|
||||||
text: 'Yes',
|
text: 'Yes',
|
||||||
@ -83,7 +70,7 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
|
function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
const message =
|
const message =
|
||||||
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?';
|
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?';
|
||||||
const actions = [
|
const actions = [
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import { DicomMetadataStore, DICOMSR, utils } from '@ohif/core';
|
|||||||
import { useDebounce } from '@hooks';
|
import { useDebounce } from '@hooks';
|
||||||
import ActionButtons from './ActionButtons';
|
import ActionButtons from './ActionButtons';
|
||||||
import { useTrackedMeasurements } from '../../getContextModule';
|
import { useTrackedMeasurements } from '../../getContextModule';
|
||||||
import createReportAsync from './../../_shared/createReportAsync.js';
|
import createReportDialogPrompt from '../../_shared/createReportDialogPrompt';
|
||||||
import setCornerstoneMeasurementActive from '../../_shared/setCornerstoneMeasurementActive';
|
import RESPONSES from '../../_shared/PROMPT_RESPONSES';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
|
|
||||||
@ -32,11 +32,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
measurementChangeTimestamp,
|
measurementChangeTimestamp,
|
||||||
200
|
200
|
||||||
);
|
);
|
||||||
const {
|
const { MeasurementService, UIDialogService } = servicesManager.services;
|
||||||
MeasurementService,
|
|
||||||
UIDialogService,
|
|
||||||
DisplaySetService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
const [
|
const [
|
||||||
trackedMeasurements,
|
trackedMeasurements,
|
||||||
sendTrackedMeasurementsEvent,
|
sendTrackedMeasurementsEvent,
|
||||||
@ -132,11 +128,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
};
|
};
|
||||||
}, [MeasurementService, sendTrackedMeasurementsEvent]);
|
}, [MeasurementService, sendTrackedMeasurementsEvent]);
|
||||||
|
|
||||||
function createReport() {
|
async function exportReport() {
|
||||||
// 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 measurements = MeasurementService.getMeasurements();
|
||||||
const trackedMeasurements = measurements.filter(
|
const trackedMeasurements = measurements.filter(
|
||||||
m =>
|
m =>
|
||||||
@ -144,20 +136,21 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
trackedSeries.includes(m.referenceSeriesUID)
|
trackedSeries.includes(m.referenceSeriesUID)
|
||||||
);
|
);
|
||||||
|
|
||||||
return createReportAsync(servicesManager, dataSource, trackedMeasurements);
|
const promptResult = await createReportDialogPrompt(UIDialogService);
|
||||||
}
|
|
||||||
|
|
||||||
function exportReport() {
|
if (promptResult.action === RESPONSES.CREATE_REPORT) {
|
||||||
const measurements = MeasurementService.getMeasurements();
|
const additionalFindings = ['ArrowAnnotate'];
|
||||||
const trackedMeasurements = measurements.filter(
|
|
||||||
m =>
|
|
||||||
trackedStudy === m.referenceStudyUID &&
|
|
||||||
trackedSeries.includes(m.referenceSeriesUID)
|
|
||||||
);
|
|
||||||
|
|
||||||
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 }) => {
|
const jumpToImage = ({ id, isActive }) => {
|
||||||
@ -273,7 +266,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
<div className="flex justify-center p-4">
|
<div className="flex justify-center p-4">
|
||||||
<ActionButtons
|
<ActionButtons
|
||||||
onExportClick={exportReport}
|
onExportClick={exportReport}
|
||||||
onCreateReportClick={createReport}
|
onCreateReportClick={() => {
|
||||||
|
sendTrackedMeasurementsEvent('SAVE_REPORT', {
|
||||||
|
viewportIndex: viewportGrid.activeViewportIndex,
|
||||||
|
isBackupSave: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -448,7 +448,7 @@ function _createStudyBrowserTabs(
|
|||||||
ds => ds.StudyInstanceUID === study.studyInstanceUid
|
ds => ds.StudyInstanceUID === study.studyInstanceUid
|
||||||
);
|
);
|
||||||
|
|
||||||
displaySetsForStudy.sort((a,b) => {
|
displaySetsForStudy.sort((a, b) => {
|
||||||
if (a.seriesNumber !== b.seriesNumber) {
|
if (a.seriesNumber !== b.seriesNumber) {
|
||||||
return a.seriesNumber - b.seriesNumber;
|
return a.seriesNumber - b.seriesNumber;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
stowSRFromMeasurements,
|
stowSRFromMeasurements,
|
||||||
} from './handleStructuredReport';
|
} from './handleStructuredReport';
|
||||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
@ -47,11 +48,20 @@ const retrieveMeasurements = server => {
|
|||||||
*
|
*
|
||||||
* @param {object[]} measurementData An array of measurements from the measurements service
|
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
* @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.
|
* as opposed to Finding Sites.
|
||||||
* that you wish to serialize.
|
* that you wish to serialize.
|
||||||
*/
|
*/
|
||||||
const downloadReport = (measurementData, additionalFindingTypes = []) => {
|
const downloadReport = (
|
||||||
const srDataset = generateReport(measurementData, additionalFindingTypes);
|
measurementData,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options = {}
|
||||||
|
) => {
|
||||||
|
const srDataset = generateReport(
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options
|
||||||
|
);
|
||||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||||
|
|
||||||
//Create a URL for the binary.
|
//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
|
* @param {object[]} measurementData An array of measurements from the measurements service
|
||||||
* that you wish to serialize.
|
* that you wish to serialize.
|
||||||
* @param {string[]} additionalFindingTypes toolTypes that should be stored with labels as Findings
|
* @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(
|
const filteredToolState = _getFilteredCornerstoneToolState(
|
||||||
measurementData,
|
measurementData,
|
||||||
additionalFindingTypes
|
additionalFindingTypes
|
||||||
);
|
);
|
||||||
|
|
||||||
const report = MeasurementReport.generateReport(
|
const report = MeasurementReport.generateReport(
|
||||||
filteredToolState,
|
filteredToolState,
|
||||||
cornerstone.metaData
|
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.
|
* that you wish to serialize.
|
||||||
* @param {object} dataSource The dataSource that you wish to use to persist the data.
|
* @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
|
* @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
|
* @return {object} The naturalized report
|
||||||
*/
|
*/
|
||||||
const storeMeasurements = async (
|
const storeMeasurements = async (
|
||||||
measurementData,
|
measurementData,
|
||||||
dataSource,
|
dataSource,
|
||||||
additionalFindingTypes = []
|
additionalFindingTypes,
|
||||||
|
options = {}
|
||||||
) => {
|
) => {
|
||||||
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
|
// 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.
|
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||||
@ -105,7 +126,8 @@ const storeMeasurements = async (
|
|||||||
try {
|
try {
|
||||||
const naturalizedReport = generateReport(
|
const naturalizedReport = generateReport(
|
||||||
measurementData,
|
measurementData,
|
||||||
additionalFindingTypes
|
additionalFindingTypes,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
const { StudyInstanceUID } = naturalizedReport;
|
const { StudyInstanceUID } = naturalizedReport;
|
||||||
|
|
||||||
@ -117,6 +139,7 @@ const storeMeasurements = async (
|
|||||||
|
|
||||||
return naturalizedReport;
|
return naturalizedReport;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
log.error(
|
log.error(
|
||||||
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||||
);
|
);
|
||||||
@ -124,6 +147,8 @@ const storeMeasurements = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// _getFilteredCornerstoneToolState
|
||||||
|
// DIFFERENT IMPLEMENTATION HERE! What's up?
|
||||||
function _getFilteredCornerstoneToolState(
|
function _getFilteredCornerstoneToolState(
|
||||||
measurementData,
|
measurementData,
|
||||||
additionalFindingTypes
|
additionalFindingTypes
|
||||||
@ -147,7 +172,6 @@ function _getFilteredCornerstoneToolState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
|
const measurmentDataI = measurementData.find(md => md.id === toolDataI.id);
|
||||||
|
|
||||||
const toolData = imageIdSpecificToolState[toolType].data;
|
const toolData = imageIdSpecificToolState[toolType].data;
|
||||||
|
|
||||||
let finding;
|
let finding;
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default class ExtensionManager {
|
|||||||
this.moduleTypeNames.forEach(moduleType => {
|
this.moduleTypeNames.forEach(moduleType => {
|
||||||
this.modules[moduleType] = [];
|
this.modules[moduleType] = [];
|
||||||
});
|
});
|
||||||
this._extensionLifeCycleHooks = { onModeEnter: {} };
|
this._extensionLifeCycleHooks = { onModeEnter: {}, onModeExit: {} };
|
||||||
this.dataSourceMap = {};
|
this.dataSourceMap = {};
|
||||||
this.defaultDataSourceName = appConfig.defaultDataSourceName;
|
this.defaultDataSourceName = appConfig.defaultDataSourceName;
|
||||||
this.activeDataSource = undefined;
|
this.activeDataSource = undefined;
|
||||||
@ -29,12 +29,21 @@ export default class ExtensionManager {
|
|||||||
onModeEnter() {
|
onModeEnter() {
|
||||||
const {
|
const {
|
||||||
registeredExtensionIds,
|
registeredExtensionIds,
|
||||||
getModuleEntry,
|
|
||||||
_servicesManager,
|
_servicesManager,
|
||||||
_commandsManager,
|
_commandsManager,
|
||||||
_extensionLifeCycleHooks,
|
_extensionLifeCycleHooks,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
MeasurementService,
|
||||||
|
ViewportGridService,
|
||||||
|
HangingProtocolService,
|
||||||
|
} = _servicesManager.services;
|
||||||
|
|
||||||
|
MeasurementService.clearMeasurements();
|
||||||
|
ViewportGridService.reset();
|
||||||
|
HangingProtocolService.reset();
|
||||||
|
|
||||||
registeredExtensionIds.forEach(extensionId => {
|
registeredExtensionIds.forEach(extensionId => {
|
||||||
const onModeEnter = _extensionLifeCycleHooks.onModeEnter[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
|
* An array of extensions, or an array of arrays that contains extension
|
||||||
* configuration pairs.
|
* configuration pairs.
|
||||||
@ -110,6 +149,11 @@ export default class ExtensionManager {
|
|||||||
extension.onModeEnter;
|
extension.onModeEnter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extension.onModeExit) {
|
||||||
|
this._extensionLifeCycleHooks.onModeExit[extensionId] =
|
||||||
|
extension.onModeExit;
|
||||||
|
}
|
||||||
|
|
||||||
// Register Modules
|
// Register Modules
|
||||||
this.moduleTypeNames.forEach(moduleType => {
|
this.moduleTypeNames.forEach(moduleType => {
|
||||||
const extensionModule = this._getExtensionModule(
|
const extensionModule = this._getExtensionModule(
|
||||||
|
|||||||
@ -77,10 +77,10 @@ class ConformanceCriteria {
|
|||||||
this.nonconformities = nonconformities;
|
this.nonconformities = nonconformities;
|
||||||
this.groupedNonConformities = groupedNonConformities;
|
this.groupedNonConformities = groupedNonConformities;
|
||||||
|
|
||||||
console.warn('nonconformities');
|
// console.warn('nonconformities');
|
||||||
console.warn(nonconformities);
|
// console.warn(nonconformities);
|
||||||
console.warn('groupedNonConformities');
|
// console.warn('groupedNonConformities');
|
||||||
console.warn(groupedNonConformities);
|
// console.warn(groupedNonConformities);
|
||||||
|
|
||||||
return nonconformities;
|
return nonconformities;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,9 +57,7 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
|||||||
const BaseImplementation = {
|
const BaseImplementation = {
|
||||||
EVENTS,
|
EVENTS,
|
||||||
listeners: {},
|
listeners: {},
|
||||||
// TODO: The assumption is that this is called per Study per Series
|
addInstances(instances, madeInClient = false) {
|
||||||
// We should do more to verify/clarify that
|
|
||||||
addInstances(instances) {
|
|
||||||
const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
|
const { StudyInstanceUID, SeriesInstanceUID } = instances[0];
|
||||||
|
|
||||||
let study = _model.studies.find(
|
let study = _model.studies.find(
|
||||||
@ -81,6 +79,7 @@ const BaseImplementation = {
|
|||||||
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
this._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
madeInClient,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addSeriesMetadata(seriesSummaryMetadata) {
|
addSeriesMetadata(seriesSummaryMetadata) {
|
||||||
|
|||||||
@ -36,6 +36,10 @@ export default class DisplaySetService {
|
|||||||
return displaySetCache;
|
return displaySetCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMostRecentDisplaySet() {
|
||||||
|
return this.activeDisplaySets[this.activeDisplaySets.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
getActiveDisplaySets() {
|
getActiveDisplaySets() {
|
||||||
return this.activeDisplaySets;
|
return this.activeDisplaySets;
|
||||||
}
|
}
|
||||||
@ -69,6 +73,12 @@ export default class DisplaySetService {
|
|||||||
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUid
|
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} input
|
||||||
|
* @param {*} param1
|
||||||
|
* @returns {string[]} - added displaySetInstanceUIDs
|
||||||
|
*/
|
||||||
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
|
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
|
||||||
if (!input || !input.length) {
|
if (!input || !input.length) {
|
||||||
throw new Error('No instances were provided.');
|
throw new Error('No instances were provided.');
|
||||||
@ -84,11 +94,12 @@ export default class DisplaySetService {
|
|||||||
let displaySetsAdded = [];
|
let displaySetsAdded = [];
|
||||||
|
|
||||||
if (batch) {
|
if (batch) {
|
||||||
input.forEach(instances => {
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const instances = input[i];
|
||||||
const displaySets = this.makeDisplaySetForInstances(instances);
|
const displaySets = this.makeDisplaySetForInstances(instances);
|
||||||
|
|
||||||
displaySetsAdded = [...displaySetsAdded, displaySets];
|
displaySetsAdded = [...displaySetsAdded, displaySets];
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
const displaySets = this.makeDisplaySetForInstances(input);
|
const displaySets = this.makeDisplaySetForInstances(input);
|
||||||
|
|
||||||
@ -109,6 +120,8 @@ export default class DisplaySetService {
|
|||||||
displaySetsAdded,
|
displaySetsAdded,
|
||||||
options,
|
options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return displaySetsAdded;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@ const ViewportActionBar = ({
|
|||||||
showNavArrows,
|
showNavArrows,
|
||||||
showPatientInfo: patientInfoVisibility,
|
showPatientInfo: patientInfoVisibility,
|
||||||
onSeriesChange,
|
onSeriesChange,
|
||||||
onHydrationClick,
|
|
||||||
onDoubleClick,
|
onDoubleClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility);
|
||||||
@ -32,7 +31,6 @@ const ViewportActionBar = ({
|
|||||||
label,
|
label,
|
||||||
isTracked,
|
isTracked,
|
||||||
isLocked,
|
isLocked,
|
||||||
isHydrated,
|
|
||||||
modality,
|
modality,
|
||||||
studyDate,
|
studyDate,
|
||||||
currentSeries,
|
currentSeries,
|
||||||
@ -78,13 +76,7 @@ const ViewportActionBar = ({
|
|||||||
Measurements cannot be duplicated.
|
Measurements cannot be duplicated.
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: () => (
|
: () => <div>This SR is unlocked.</div>;
|
||||||
<div>
|
|
||||||
This SR is unlocked. <br />
|
|
||||||
You can duplicate measurements on your current report <br /> by
|
|
||||||
clicking 'Edit'.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip content={<TooltipMessage />} position="bottom-left">
|
<Tooltip content={<TooltipMessage />} position="bottom-left">
|
||||||
@ -101,16 +93,6 @@ const ViewportActionBar = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</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
|
// TODO: For some reason this is running before the Providers
|
||||||
// are calling setServiceImplementation
|
// are calling setServiceImplementation
|
||||||
// TOOD -> iterate through services.
|
// TOOD -> iterate through services.
|
||||||
MeasurementService.clearMeasurements();
|
|
||||||
ViewportGridService.reset();
|
|
||||||
HangingProtocolService.reset();
|
|
||||||
|
|
||||||
// Extension
|
// Extension
|
||||||
extensionManager.onModeEnter();
|
extensionManager.onModeEnter();
|
||||||
// Mode
|
// Mode
|
||||||
route.init({ servicesManager, extensionManager });
|
route.init({ servicesManager, extensionManager });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
extensionManager.onModeExit();
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
mode,
|
mode,
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
@ -131,13 +132,16 @@ export default function ModeRoute({
|
|||||||
// DisplaySetService would wire this up?
|
// DisplaySetService would wire this up?
|
||||||
const { unsubscribe } = DicomMetadataStore.subscribe(
|
const { unsubscribe } = DicomMetadataStore.subscribe(
|
||||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||||
({ StudyInstanceUID, SeriesInstanceUID }) => {
|
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
|
||||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID
|
SeriesInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
DisplaySetService.makeDisplaySets(seriesMetadata.instances);
|
DisplaySetService.makeDisplaySets(
|
||||||
|
seriesMetadata.instances,
|
||||||
|
madeInClient
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -161,9 +165,7 @@ export default function ModeRoute({
|
|||||||
const { unsubscribe } = DicomMetadataStore.subscribe(
|
const { unsubscribe } = DicomMetadataStore.subscribe(
|
||||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||||
({ StudyInstanceUID }) => {
|
({ StudyInstanceUID }) => {
|
||||||
const studyMetadata = DicomMetadataStore.getStudy(
|
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
StudyInstanceUID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortedSeries = studyMetadata.series.sort((a, b) => {
|
const sortedSeries = studyMetadata.series.sort((a, b) => {
|
||||||
const aLowPriority = isLowPriorityModality(a.Modality);
|
const aLowPriority = isLowPriorityModality(a.Modality);
|
||||||
@ -176,13 +178,12 @@ export default function ModeRoute({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return a.SeriesNumber - b.SeriesNumber;
|
return a.SeriesNumber - b.SeriesNumber;
|
||||||
})
|
});
|
||||||
|
|
||||||
const { SeriesInstanceUID } = sortedSeries[0]
|
const { SeriesInstanceUID } = sortedSeries[0];
|
||||||
|
|
||||||
HangingProtocolService.setHangingProtocol(
|
HangingProtocolService.setHangingProtocol({
|
||||||
{
|
/*protocolMatchingRules: [
|
||||||
/*protocolMatchingRules: [
|
|
||||||
{
|
{
|
||||||
id: '7tmuq7KzDMCWFeapc',
|
id: '7tmuq7KzDMCWFeapc',
|
||||||
weight: 2,
|
weight: 2,
|
||||||
@ -195,9 +196,9 @@ export default function ModeRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],*/
|
],*/
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
/*id: 'v5PfGt9F6mffZPif5',
|
/*id: 'v5PfGt9F6mffZPif5',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
@ -206,30 +207,29 @@ export default function ModeRoute({
|
|||||||
},
|
},
|
||||||
layoutTemplateName: 'gridLayout',
|
layoutTemplateName: 'gridLayout',
|
||||||
},*/
|
},*/
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportSettings: {},
|
viewportSettings: {},
|
||||||
imageMatchingRules: [],
|
imageMatchingRules: [],
|
||||||
seriesMatchingRules: [
|
seriesMatchingRules: [
|
||||||
{
|
{
|
||||||
id: 'mXnsCcNzZL56z7mTZ',
|
id: 'mXnsCcNzZL56z7mTZ',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
required: true,
|
required: true,
|
||||||
attribute: 'SeriesInstanceUID',
|
attribute: 'SeriesInstanceUID',
|
||||||
constraint: {
|
constraint: {
|
||||||
equals: {
|
equals: {
|
||||||
value: SeriesInstanceUID,
|
value: SeriesInstanceUID,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
studyMatchingRules: [],
|
],
|
||||||
},
|
studyMatchingRules: [],
|
||||||
],
|
},
|
||||||
}
|
],
|
||||||
]
|
},
|
||||||
}
|
],
|
||||||
)
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user