diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.ts b/extensions/cornerstone-dicom-sr/src/commandsModule.ts index 43a6efb3c..d3febffa3 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.ts +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.ts @@ -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; }; diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 0c67cd0e5..c8da45531 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -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, diff --git a/extensions/default/src/Panels/createReportDialogPrompt.tsx b/extensions/default/src/Panels/createReportDialogPrompt.tsx index 5fbd738ab..17a0316ba 100644 --- a/extensions/default/src/Panels/createReportDialogPrompt.tsx +++ b/extensions/default/src/Panels/createReportDialogPrompt.tsx @@ -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, }); }, diff --git a/extensions/default/src/customizations/reportDialogCustomization.tsx b/extensions/default/src/customizations/reportDialogCustomization.tsx index 9ebbd59f4..478cfa1d4 100644 --- a/extensions/default/src/customizations/reportDialogCustomization.tsx +++ b/extensions/default/src/customizations/reportDialogCustomization.tsx @@ -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( dataSources?.[0]?.value ?? null ); - + const [selectedSeries, setSelectedSeries] = useState(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 (
-
+
{showDataSourceSelect && ( -
+
+
Data source
+ + + + + {seriesOptions.map(series => ( + + {series.label} + + ))} + + +
+
+
+
- +
diff --git a/extensions/default/src/utils/findSRWithSameSeriesDescription.ts b/extensions/default/src/utils/findSRWithSameSeriesDescription.ts deleted file mode 100644 index af565c9aa..000000000 --- a/extensions/default/src/utils/findSRWithSameSeriesDescription.ts +++ /dev/null @@ -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 }; -} diff --git a/extensions/default/src/utils/getCurrentDicomDateTime.ts b/extensions/default/src/utils/getCurrentDicomDateTime.ts new file mode 100644 index 000000000..2266998eb --- /dev/null +++ b/extensions/default/src/utils/getCurrentDicomDateTime.ts @@ -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 }; +}; diff --git a/extensions/default/src/utils/getNextSRSeriesNumber.js b/extensions/default/src/utils/getNextSRSeriesNumber.js deleted file mode 100644 index c980aae9c..000000000 --- a/extensions/default/src/utils/getNextSRSeriesNumber.js +++ /dev/null @@ -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; -} diff --git a/extensions/default/src/utils/getSRSeriesAndInstanceNumber.js b/extensions/default/src/utils/getSRSeriesAndInstanceNumber.js new file mode 100644 index 000000000..b60722c6d --- /dev/null +++ b/extensions/default/src/utils/getSRSeriesAndInstanceNumber.js @@ -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, + }; +} diff --git a/extensions/default/src/utils/promptSaveReport.tsx b/extensions/default/src/utils/promptSaveReport.tsx index 22b2905fc..f2e0dffe0 100644 --- a/extensions/default/src/utils/promptSaveReport.tsx +++ b/extensions/default/src/utils/promptSaveReport.tsx @@ -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'