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 { MeasurementReport } = adaptersSR.Cornerstone3D;
|
||||||
const { log } = OHIF;
|
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
|
* @param measurementData An array of measurements from the measurements service
|
||||||
* that you wish to serialize.
|
* that you wish to serialize.
|
||||||
@ -17,7 +26,7 @@ const { log } = OHIF;
|
|||||||
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
* @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(
|
const filteredToolState = getFilteredCornerstoneToolState(
|
||||||
measurementData,
|
measurementData,
|
||||||
additionalFindingTypes
|
additionalFindingTypes
|
||||||
@ -37,6 +46,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {})
|
|||||||
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
if (typeof dataset.SpecificCharacterSet === 'undefined') {
|
||||||
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
dataset.SpecificCharacterSet = 'ISO_IR 192';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataset.InstanceNumber = options.InstanceNumber ?? 1;
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -90,6 +90,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
SeriesDate,
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
ConceptNameCodeSequence,
|
ConceptNameCodeSequence,
|
||||||
SOPClassUID,
|
SOPClassUID,
|
||||||
} = instance;
|
} = instance;
|
||||||
@ -106,6 +107,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
SeriesDate,
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ export default function CreateReportDialogPrompt({
|
|||||||
}): Promise<{
|
}): Promise<{
|
||||||
value: string;
|
value: string;
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
|
series: string;
|
||||||
action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES];
|
action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES];
|
||||||
}> {
|
}> {
|
||||||
const { uiDialogService, customizationService } = servicesManager.services;
|
const { uiDialogService, customizationService } = servicesManager.services;
|
||||||
@ -22,10 +23,11 @@ export default function CreateReportDialogPrompt({
|
|||||||
content: ReportDialog,
|
content: ReportDialog,
|
||||||
contentProps: {
|
contentProps: {
|
||||||
dataSources: allowMultipleDataSources ? dataSources : undefined,
|
dataSources: allowMultipleDataSources ? dataSources : undefined,
|
||||||
onSave: async ({ reportName, dataSource: selectedDataSource }) => {
|
onSave: async ({ reportName, dataSource: selectedDataSource, series }) => {
|
||||||
resolve({
|
resolve({
|
||||||
value: reportName,
|
value: reportName,
|
||||||
dataSourceName: selectedDataSource,
|
dataSourceName: selectedDataSource,
|
||||||
|
series,
|
||||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -33,6 +35,7 @@ export default function CreateReportDialogPrompt({
|
|||||||
resolve({
|
resolve({
|
||||||
action: PROMPT_RESPONSES.CANCEL,
|
action: PROMPT_RESPONSES.CANCEL,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
|
series: undefined,
|
||||||
dataSourceName: 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 { InputDialog } from '@ohif/ui-next';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ohif/ui-next';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ohif/ui-next';
|
||||||
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
type DataSource = {
|
type DataSource = {
|
||||||
value: string;
|
value: string;
|
||||||
@ -11,21 +12,53 @@ type DataSource = {
|
|||||||
type ReportDialogProps = {
|
type ReportDialogProps = {
|
||||||
dataSources: DataSource[];
|
dataSources: DataSource[];
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
onSave: (data: { reportName: string; dataSource: string | null }) => void;
|
onSave: (data: { reportName: string; dataSource: string | null; series: string | null }) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) {
|
function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) {
|
||||||
|
const { servicesManager } = useSystem();
|
||||||
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
|
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(
|
||||||
dataSources?.[0]?.value ?? null
|
dataSources?.[0]?.value ?? null
|
||||||
);
|
);
|
||||||
|
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
|
||||||
const [reportName, setReportName] = useState('');
|
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 = () => {
|
const handleSave = () => {
|
||||||
onSave({
|
onSave({
|
||||||
reportName,
|
reportName,
|
||||||
dataSource: selectedDataSource,
|
dataSource: selectedDataSource,
|
||||||
|
series: selectedSeries,
|
||||||
});
|
});
|
||||||
hide();
|
hide();
|
||||||
};
|
};
|
||||||
@ -40,9 +73,10 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
|||||||
return (
|
return (
|
||||||
<div className="text-foreground mt-2 flex min-w-[400px] max-w-md flex-col gap-4">
|
<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="flex flex-col gap-3">
|
||||||
<div className={showDataSourceSelect ? 'flex gap-4' : ''}>
|
<div className="flex gap-4">
|
||||||
{showDataSourceSelect && (
|
{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
|
<Select
|
||||||
value={selectedDataSource}
|
value={selectedDataSource}
|
||||||
onValueChange={setSelectedDataSource}
|
onValueChange={setSelectedDataSource}
|
||||||
@ -63,14 +97,40 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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
|
<InputDialog
|
||||||
value={reportName}
|
value={reportName}
|
||||||
onChange={setReportName}
|
onChange={setReportName}
|
||||||
submitOnEnter
|
submitOnEnter
|
||||||
>
|
>
|
||||||
<InputDialog.Field>
|
<InputDialog.Field>
|
||||||
<InputDialog.Input placeholder="Report name" />
|
<InputDialog.Input
|
||||||
|
placeholder="Report name"
|
||||||
|
disabled={!!selectedSeries}
|
||||||
|
/>
|
||||||
</InputDialog.Field>
|
</InputDialog.Field>
|
||||||
</InputDialog>
|
</InputDialog>
|
||||||
</div>
|
</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 createReportAsync from '../Actions/createReportAsync';
|
||||||
import { createReportDialogPrompt } from '../Panels';
|
import { createReportDialogPrompt } from '../Panels';
|
||||||
import getNextSRSeriesNumber from './getNextSRSeriesNumber';
|
|
||||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||||
|
import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber';
|
||||||
|
import { getSeriesDateTime } from './getCurrentDicomDateTime';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filterAnd,
|
filterAnd,
|
||||||
@ -27,7 +28,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
|||||||
filterMeasurementsBySeriesUID(trackedSeries),
|
filterMeasurementsBySeriesUID(trackedSeries),
|
||||||
filterPlanarMeasurement
|
filterPlanarMeasurement
|
||||||
),
|
),
|
||||||
defaultSaveTitle = 'Research Derived Series',
|
defaultSaveTitle = 'Create Report',
|
||||||
} = ctx;
|
} = ctx;
|
||||||
let displaySetInstanceUIDs;
|
let displaySetInstanceUIDs;
|
||||||
|
|
||||||
@ -45,7 +46,12 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
|||||||
|
|
||||||
const SeriesDescription = promptResult.value || defaultSaveTitle;
|
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 () => {
|
const getReport = async () => {
|
||||||
return commandsManager.runCommand(
|
return commandsManager.runCommand(
|
||||||
@ -57,6 +63,10 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
|||||||
options: {
|
options: {
|
||||||
SeriesDescription,
|
SeriesDescription,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
InstanceNumber,
|
||||||
|
SeriesInstanceUID: promptResult.series,
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'CORNERSTONE_STRUCTURED_REPORT'
|
'CORNERSTONE_STRUCTURED_REPORT'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user