feat: allow saving to same SR series (#4889)
This commit is contained in:
parent
7b40963997
commit
62e5a62066
@ -10,6 +10,15 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||
const { log } = OHIF;
|
||||
|
||||
interface Options {
|
||||
SeriesDescription?: string;
|
||||
SeriesInstanceUID?: string;
|
||||
SeriesNumber?: number;
|
||||
InstanceNumber?: number;
|
||||
SeriesDate?: string;
|
||||
SeriesTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param measurementData An array of measurements from the measurements service
|
||||
* that you wish to serialize.
|
||||
@ -17,7 +26,7 @@ const { log } = OHIF;
|
||||
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
*
|
||||
*/
|
||||
const _generateReport = (measurementData, additionalFindingTypes, options = {}) => {
|
||||
const _generateReport = (measurementData, additionalFindingTypes, options: Options = {}) => {
|
||||
const filteredToolState = getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
@ -37,6 +46,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
||||
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||
}
|
||||
|
||||
dataset.InstanceNumber = options.InstanceNumber ?? 1;
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
|
||||
@ -90,6 +90,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
ConceptNameCodeSequence,
|
||||
SOPClassUID,
|
||||
} = instance;
|
||||
@ -106,6 +107,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
|
||||
@ -7,6 +7,7 @@ export default function CreateReportDialogPrompt({
|
||||
}): Promise<{
|
||||
value: string;
|
||||
dataSourceName: string;
|
||||
series: string;
|
||||
action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES];
|
||||
}> {
|
||||
const { uiDialogService, customizationService } = servicesManager.services;
|
||||
@ -22,10 +23,11 @@ export default function CreateReportDialogPrompt({
|
||||
content: ReportDialog,
|
||||
contentProps: {
|
||||
dataSources: allowMultipleDataSources ? dataSources : undefined,
|
||||
onSave: async ({ reportName, dataSource: selectedDataSource }) => {
|
||||
onSave: async ({ reportName, dataSource: selectedDataSource, series }) => {
|
||||
resolve({
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
series,
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
});
|
||||
},
|
||||
@ -33,6 +35,7 @@ export default function CreateReportDialogPrompt({
|
||||
resolve({
|
||||
action: PROMPT_RESPONSES.CANCEL,
|
||||
value: undefined,
|
||||
series: undefined,
|
||||
dataSourceName: undefined,
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { InputDialog } from '@ohif/ui-next';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ohif/ui-next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
type DataSource = {
|
||||
value: string;
|
||||
@ -11,21 +12,53 @@ type DataSource = {
|
||||
type ReportDialogProps = {
|
||||
dataSources: DataSource[];
|
||||
hide: () => void;
|
||||
onSave: (data: { reportName: string; dataSource: string | null }) => void;
|
||||
onSave: (data: { reportName: string; dataSource: string | null; series: string | null }) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) {
|
||||
const { servicesManager } = useSystem();
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
|
||||
dataSources?.[0]?.value ?? null
|
||||
);
|
||||
|
||||
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
|
||||
const [reportName, setReportName] = useState('');
|
||||
|
||||
const { displaySetService } = servicesManager.services;
|
||||
|
||||
const seriesOptions = useMemo(() => {
|
||||
const displaySetsMap = displaySetService.getDisplaySetCache();
|
||||
const displaySets = Array.from(displaySetsMap.values());
|
||||
const options = displaySets
|
||||
.filter(ds => ds.Modality === 'SR')
|
||||
.map(ds => ({
|
||||
value: ds.SeriesInstanceUID,
|
||||
description: ds.SeriesDescription,
|
||||
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
value: null,
|
||||
description: null,
|
||||
label: 'Create new series',
|
||||
},
|
||||
...options,
|
||||
];
|
||||
}, [displaySetService]);
|
||||
|
||||
useEffect(() => {
|
||||
const seriesOption = seriesOptions.find(s => s.value === selectedSeries);
|
||||
const newReportName =
|
||||
selectedSeries && seriesOption?.description ? seriesOption.description : '';
|
||||
setReportName(newReportName);
|
||||
}, [selectedSeries, seriesOptions]);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
series: selectedSeries,
|
||||
});
|
||||
hide();
|
||||
};
|
||||
@ -40,9 +73,10 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
return (
|
||||
<div className="text-foreground mt-2 flex min-w-[400px] max-w-md flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className={showDataSourceSelect ? 'flex gap-4' : ''}>
|
||||
<div className="flex gap-4">
|
||||
{showDataSourceSelect && (
|
||||
<div className="mt-1 w-1/3">
|
||||
<div className="mt-1 w-1/2">
|
||||
<div className="mb-1 pl-1 font-light">Data source</div>
|
||||
<Select
|
||||
value={selectedDataSource}
|
||||
onValueChange={setSelectedDataSource}
|
||||
@ -63,14 +97,40 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className={showDataSourceSelect ? 'mt-1 w-2/3' : 'w-full'}>
|
||||
<div className={showDataSourceSelect ? 'mt-1 w-1/2' : 'mt-1 w-full'}>
|
||||
<div className="mb-1 pl-1 font-light">Series</div>
|
||||
<Select
|
||||
value={selectedSeries}
|
||||
onValueChange={setSelectedSeries}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a series" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{seriesOptions.map(series => (
|
||||
<SelectItem
|
||||
key={series.value}
|
||||
value={series.value}
|
||||
>
|
||||
{series.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className={showDataSourceSelect ? 'flex gap-4' : ''}>
|
||||
<div className="mt-4 w-full">
|
||||
<InputDialog
|
||||
value={reportName}
|
||||
onChange={setReportName}
|
||||
submitOnEnter
|
||||
>
|
||||
<InputDialog.Field>
|
||||
<InputDialog.Input placeholder="Report name" />
|
||||
<InputDialog.Input
|
||||
placeholder="Report name"
|
||||
disabled={!!selectedSeries}
|
||||
/>
|
||||
</InputDialog.Field>
|
||||
</InputDialog>
|
||||
</div>
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
import { DisplaySetService, Types } from '@ohif/core';
|
||||
|
||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||
|
||||
/**
|
||||
* Find an SR having the same series description.
|
||||
* This is used by the store service in order to store DICOM SR's having the
|
||||
* same Series Description into a single series under consecutive instance numbers
|
||||
* That way, they are all organized as a set and could have tools to view
|
||||
* "prior" SR instances.
|
||||
*
|
||||
* @param SeriesDescription - is the description to look for
|
||||
* @param displaySetService - the display sets to search for DICOM SR in
|
||||
* @returns SeriesMetadata from a DICOM SR having the same series description
|
||||
*/
|
||||
export default function findSRWithSameSeriesDescription(
|
||||
SeriesDescription: string,
|
||||
displaySetService: DisplaySetService
|
||||
): Types.SeriesMetadata {
|
||||
const activeDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
const srDisplaySets = activeDisplaySets.filter(ds => ds.Modality === 'SR');
|
||||
const sameSeries = srDisplaySets.find(ds => ds.SeriesDescription === SeriesDescription);
|
||||
if (sameSeries) {
|
||||
console.log('Storing to same series', sameSeries);
|
||||
const { instance } = sameSeries;
|
||||
const { SeriesInstanceUID, SeriesDescription, SeriesDate, SeriesTime, SeriesNumber, Modality } =
|
||||
instance;
|
||||
return {
|
||||
SeriesInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
Modality,
|
||||
InstanceNumber: sameSeries.instances.length + 1,
|
||||
};
|
||||
}
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
return { SeriesDescription, SeriesNumber };
|
||||
}
|
||||
20
extensions/default/src/utils/getCurrentDicomDateTime.ts
Normal file
20
extensions/default/src/utils/getCurrentDicomDateTime.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export const getSeriesDateTime = (jsDate: Date = new Date()) => {
|
||||
const dicomDateTime = getDicomDateTime(jsDate);
|
||||
return {
|
||||
SeriesDate: dicomDateTime.date,
|
||||
SeriesTime: dicomDateTime.time,
|
||||
};
|
||||
};
|
||||
|
||||
export const getDicomDateTime = (jsDate: Date = new Date()) => {
|
||||
const month = String(jsDate.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(jsDate.getUTCDate()).padStart(2, '0');
|
||||
const year = String(jsDate.getUTCFullYear()).padStart(4, '0');
|
||||
const date = `${year}${month}${day}`;
|
||||
const hours = String(jsDate.getUTCHours()).padStart(2, '0');
|
||||
const minutes = String(jsDate.getUTCMinutes()).padStart(2, '0');
|
||||
const seconds = String(jsDate.getUTCSeconds()).padStart(2, '0');
|
||||
const time = `${hours}${minutes}${seconds}`;
|
||||
|
||||
return { date, time };
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
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;
|
||||
}
|
||||
48
extensions/default/src/utils/getSRSeriesAndInstanceNumber.js
Normal file
48
extensions/default/src/utils/getSRSeriesAndInstanceNumber.js
Normal file
@ -0,0 +1,48 @@
|
||||
const MIN_SR_SERIES_NUMBER = 4700;
|
||||
|
||||
export function getNextSeriesNumber({ displaySetService, modality, minSeriesNumber }) {
|
||||
const activeDisplaySets = displaySetService.getActiveDisplaySets();
|
||||
const modalityDisplaySets = activeDisplaySets.filter(ds => ds.Modality === modality);
|
||||
const modalitySeriesNumbers = modalityDisplaySets.map(ds => ds.SeriesNumber);
|
||||
const maxSeriesNumber = Math.max(...modalitySeriesNumbers, minSeriesNumber);
|
||||
|
||||
const allSeriesNumbers = activeDisplaySets.map(ds => ds.SeriesNumber);
|
||||
|
||||
let finalSeriesNumber = maxSeriesNumber + 1;
|
||||
while (allSeriesNumbers.includes(finalSeriesNumber)) {
|
||||
finalSeriesNumber++;
|
||||
}
|
||||
|
||||
return { SeriesNumber: finalSeriesNumber, InstanceNumber: 1 };
|
||||
}
|
||||
|
||||
export function getSRSeriesAndInstanceNumber({ displaySetService, SeriesInstanceUid }) {
|
||||
if (!SeriesInstanceUid) {
|
||||
return getNextSeriesNumber({
|
||||
displaySetService,
|
||||
modality: 'SR',
|
||||
minSeriesNumber: MIN_SR_SERIES_NUMBER,
|
||||
});
|
||||
}
|
||||
|
||||
const displaySetsMap = displaySetService.getDisplaySetCache();
|
||||
const displaySets = Array.from(displaySetsMap.values());
|
||||
const srDisplaySet = displaySets.find(
|
||||
ds => ds.Modality === 'SR' && ds.SeriesInstanceUID === SeriesInstanceUid
|
||||
);
|
||||
const InstanceNumber = srDisplaySet.instances?.length + 1;
|
||||
|
||||
if (!srDisplaySet?.SeriesNumber || !InstanceNumber) {
|
||||
return getNextSeriesNumber({
|
||||
displaySetService,
|
||||
modality: 'SR',
|
||||
minSeriesNumber: MIN_SR_SERIES_NUMBER,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
SeriesNumber: srDisplaySet.SeriesNumber,
|
||||
InstanceNumber,
|
||||
referenceDisplaySet: srDisplaySet,
|
||||
};
|
||||
}
|
||||
@ -2,8 +2,9 @@ import { utils } from '@ohif/core';
|
||||
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import { createReportDialogPrompt } from '../Panels';
|
||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||
import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber';
|
||||
import { getSeriesDateTime } from './getCurrentDicomDateTime';
|
||||
|
||||
const {
|
||||
filterAnd,
|
||||
@ -27,7 +28,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
filterMeasurementsBySeriesUID(trackedSeries),
|
||||
filterPlanarMeasurement
|
||||
),
|
||||
defaultSaveTitle = 'Research Derived Series',
|
||||
defaultSaveTitle = 'Create Report',
|
||||
} = ctx;
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
@ -45,7 +46,12 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
|
||||
const SeriesDescription = promptResult.value || defaultSaveTitle;
|
||||
|
||||
const SeriesNumber = getNextSRSeriesNumber(displaySetService);
|
||||
const { SeriesNumber, InstanceNumber, referenceDisplaySet } = getSRSeriesAndInstanceNumber({
|
||||
displaySetService,
|
||||
SeriesInstanceUid: promptResult.series,
|
||||
});
|
||||
|
||||
const { SeriesDate, SeriesTime } = referenceDisplaySet ?? getSeriesDateTime();
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
@ -57,6 +63,10 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
options: {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
SeriesInstanceUID: promptResult.series,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
},
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user