Fix/sr hydration non tracking (#3080)

* fix:DICOM SR hydration - remove redundancies

feat:Allow save and restore on non-tracking view

* PR comments
This commit is contained in:
Bill Wallace authored and GitHub committed 2022-12-29 10:13:06 -05:00
1 parent ec7cdba901
commit 9a03bde0ae
21 files changed
+466 -73

No files matched your search

@@ -0,0 +1,73 @@
import React from 'react';
import { DicomMetadataStore } from '@ohif/core';
/**
*
* @param {*} servicesManager
* @param {*} dataSource
* @param {*} measurements
* @param {*} options
* @returns {string[]} displaySetInstanceUIDs
*/
async function createReportAsync(
servicesManager,
commandsManager,
dataSource,
measurements,
options
) {
const {
DisplaySetService,
UINotificationService,
UIDialogService,
} = servicesManager.services;
const loadingDialogId = UIDialogService.create({
showOverlay: true,
isDraggable: false,
centralize: true,
// TODO: Create a loading indicator component + zeplin design?
content: Loading,
});
try {
const naturalizedReport = await commandsManager.runCommand(
'storeMeasurements',
{
measurementData: measurements,
dataSource,
additionalFindingTypes: ['ArrowAnnotate'],
options,
},
'CORNERSTONE_STRUCTURED_REPORT'
);
// 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',
message: error.message || 'Failed to store measurements',
type: 'error',
});
} finally {
UIDialogService.dismiss({ id: loadingDialogId });
}
}
function Loading() {
return <div className="text-primary-active">Loading...</div>;
}
export default createReportAsync;
@@ -13,16 +13,10 @@ function ActionButtons({ onExportClick, onCreateReportClick }) {
<Button className="px-2 py-2 text-base" onClick={onExportClick}>
{t('Export CSV')}
</Button>
<Button className="px-2 py-2 text-base" onClick={onCreateReportClick}>
{t('Create Report')}
</Button>
</ButtonGroup>
{/* <Button
className="ml-2 text-base"
variant="outlined"
size="small"
color="black"
onClick={onCreateReportClick}
>
{t('Create Report')}
</Button> */}
</React.Fragment>
);
}
@@ -5,15 +5,27 @@ import ActionButtons from './ActionButtons';
import debounce from 'lodash.debounce';
import { utils } from '@ohif/core';
import createReportDialogPrompt, {
CREATE_REPORT_DIALOG_RESPONSE,
} from './createReportDialogPrompt';
import createReportAsync from '../Actions/createReportAsync';
import getNextSRSeriesNumber from '../utils/getNextSRSeriesNumber';
const { downloadCSVReport } = utils;
export default function PanelMeasurementTable({
servicesManager,
commandsManager,
extensionManager,
}) {
const [viewportGrid, viewportGridService] = useViewportGrid();
const { MeasurementService, UIDialogService } = servicesManager.services;
const { activeViewportIndex, viewports } = viewportGrid;
const {
MeasurementService,
UIDialogService,
UINotificationService,
DisplaySetService,
} = servicesManager.services;
const [displayMeasurements, setDisplayMeasurements] = useState([]);
useEffect(() => {
@@ -56,6 +68,62 @@ export default function PanelMeasurementTable({
downloadCSVReport(measurements, MeasurementService);
}
async function clearMeasurements() {
MeasurementService.clearMeasurements();
}
async function createReport() {
// filter measurements that are added to the active study
const activeViewport = viewports[activeViewportIndex];
const measurements = MeasurementService.getMeasurements();
const displaySet = DisplaySetService.getDisplaySetByUID(
activeViewport.displaySetInstanceUIDs[0]
);
const trackedMeasurements = measurements.filter(
m => displaySet.StudyInstanceUID === m.referenceStudyUID
);
if (trackedMeasurements.length <= 0) {
UINotificationService.show({
title: 'No Measurements',
message: 'No Measurements are added to the current Study.',
type: 'info',
duration: 3000,
});
return;
}
const promptResult = await createReportDialogPrompt(UIDialogService, {
extensionManager,
});
if (promptResult.action === CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT) {
const dataSources = extensionManager.getDataSources(
promptResult.dataSourceName
);
const dataSource = dataSources[0];
const SeriesDescription =
// isUndefinedOrEmpty
promptResult.value === undefined || promptResult.value === ''
? 'Research Derived Series' // default
: promptResult.value; // provided value
const SeriesNumber = getNextSRSeriesNumber(DisplaySetService);
const displaySetInstanceUIDs = await createReportAsync(
servicesManager,
commandsManager,
dataSource,
trackedMeasurements,
{
SeriesDescription,
SeriesNumber,
}
);
}
}
const jumpToImage = ({ uid, isActive }) => {
MeasurementService.jumpToMeasurement(viewportGrid.activeViewportIndex, uid);
@@ -142,7 +210,7 @@ export default function PanelMeasurementTable({
return (
<>
<div
className="overflow-x-hidden overflow-y-auto invisible-scrollbar"
className="overflow-x-hidden overflow-y-auto ohif-scrollbar"
data-cy={'measurements-panel'}
>
<MeasurementTable
@@ -155,7 +223,8 @@ export default function PanelMeasurementTable({
<div className="flex justify-center p-4">
<ActionButtons
onExportClick={exportReport}
onCreateReportClick={() => {}}
onClearMeasurementsClick={clearMeasurements}
onCreateReportClick={createReport}
/>
</div>
</>
@@ -0,0 +1,143 @@
/* eslint-disable react/display-name */
import React from 'react';
import { Dialog, Input, Select } from '@ohif/ui';
export const CREATE_REPORT_DIALOG_RESPONSE = {
CANCEL: 0,
CREATE_REPORT: 1,
};
export default function createReportDialogPrompt(
UIDialogService,
{ extensionManager }
) {
return new Promise(function (resolve, reject) {
let dialogId = undefined;
const _handleClose = () => {
// Dismiss dialog
UIDialogService.dismiss({ id: dialogId });
// Notify of cancel action
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
value: undefined,
dataSourceName: 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: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
value: value.label,
dataSourceName: value.dataSourceName,
});
break;
case 'cancel':
resolve({
action: CREATE_REPORT_DIALOG_RESPONSE.CANCEL,
value: undefined,
dataSourceName: undefined,
});
break;
}
};
const dataSourcesOpts = Object.keys(extensionManager.dataSourceMap)
.filter(ds => {
const configuration =
extensionManager.dataSourceDefs[ds]?.configuration;
const supportsStow =
configuration?.supportsStow ?? configuration?.wadoRoot;
return supportsStow;
})
.map(ds => {
return {
value: ds,
label: ds,
placeHolder: ds,
};
});
dialogId = UIDialogService.create({
centralize: true,
isDraggable: false,
content: Dialog,
useLastPosition: false,
showOverlay: true,
contentProps: {
title: 'Provide a name for your report',
value: {
label: '',
dataSourceName: extensionManager.activeDataSource,
},
noCloseButton: true,
onClose: _handleClose,
actions: [
{ id: 'cancel', text: 'Cancel', type: 'primary' },
{ id: 'save', text: 'Save', type: 'secondary' },
],
// 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: CREATE_REPORT_DIALOG_RESPONSE.CREATE_REPORT,
value: value.label,
});
}
};
return (
<>
<div className="p-4 bg-primary-dark">
{dataSourcesOpts.length > 1 && (
<Select
closeMenuOnSelect={true}
className="mr-2 bg-black border-primary-main"
options={dataSourcesOpts}
placeholder={
dataSourcesOpts.find(
option => option.value === value.dataSourceName
).placeHolder
}
value={value.dataSourceName}
onChange={evt => {
setValue(v => ({ ...v, dataSourceName: evt.value }));
}}
isClearable={false}
/>
)}
</div>
<div className="p-4 bg-primary-dark">
<Input
autoFocus
className="mt-2 bg-black border-primary-main"
type="text"
placeholder="Enter Report Name"
containerClassName="mr-2"
value={value.label}
onChange={onChangeHandler}
onKeyPress={onKeyPressHandler}
required
/>
</div>
</>
);
},
},
});
});
}
@@ -14,7 +14,16 @@ const defaultProtocol = {
// Unused currently
imageMatchingRules: [],
// Matches displaysets, NOT series
seriesMatchingRules: [],
seriesMatchingRules: [
// Try to match series with images by default, to prevent weird display
// on SEG/SR containing studies
{
attribute: 'numImageFrames',
constraint: {
greaterThan: { value: 0 },
},
},
],
studyMatchingRules: [],
},
},
@@ -40,7 +49,6 @@ const defaultProtocol = {
},
displaySets: [
{
options: [],
id: 'defaultDisplaySetId',
},
],
@@ -16,6 +16,7 @@ function getPanelModule({
<PanelMeasurementTable
commandsManager={commandsManager}
servicesManager={servicesManager}
extensionManager={extensionManager}
/>
);
};
@@ -0,0 +1,10 @@
const MIN_SR_SERIES_NUMBER = 4700;
export default function getNextSRSeriesNumber(DisplaySetService) {
const activeDisplaySets = DisplaySetService.getActiveDisplaySets();
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
const srSeriesNumbers = srDisplaySets.map(ds => ds.SeriesNumber);
const maxSeriesNumber = Math.max(...srSeriesNumbers, MIN_SR_SERIES_NUMBER);
return maxSeriesNumber + 1;
}