Fix contour annotations RTSS saving (#5545)
* [WIP] Fix contour annotations RTSS saving * fix: Export image sop image reference provider * Fixes for download with filename * Updates to fix metadata issues with CS3D * Updates to save menu * Fixing some additional re-save metadata * Add support for saving with predecessor image sequence * PR comment fixes * fix: Load RTSS after save * fix segmentation save * fix load of mixed seg/rt studies * Use frame module to get information on the frame shown * Fix study browser to use instances.length when numImageFrames not present * fix: Load of display set split from multiple non-FOR referenes * Fix RT and SEG applying to wrong series because of using referenced series sequence without checkout sop instances * Fix save seg * Update to released CS3D versions * test: Add revokeObjectURL to test download blob * Add hard dependency on mode-basic to fix build order issues * fix: build * Try to fix build segmentation * PR comments * PR comment update
This commit is contained in:
1 parent
9c5043ca2c
commit
57205703bb
68 files changed
+962
-648
No files matched your search
@@ -28,7 +28,8 @@ function PanelStudyBrowser({
|
||||
const { servicesManager, commandsManager, extensionManager } = useSystem();
|
||||
const { displaySetService, customizationService } = servicesManager.services;
|
||||
const navigate = useNavigate();
|
||||
const studyMode = (customizationService.getCustomization('studyBrowser.studyMode') as string) || 'all';
|
||||
const studyMode =
|
||||
(customizationService.getCustomization('studyBrowser.studyMode') as string) || 'all';
|
||||
|
||||
const internalImageViewer = useImageViewer();
|
||||
const StudyInstanceUIDs = internalImageViewer.StudyInstanceUIDs;
|
||||
@@ -384,7 +385,11 @@ function PanelStudyBrowser({
|
||||
|
||||
const displaySetInstanceUID = jumpToDisplaySet;
|
||||
// It is possible to navigate to a study not currently in view
|
||||
const thumbnailLocation = _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs, activeTabName);
|
||||
const thumbnailLocation = _findTabAndStudyOfDisplaySet(
|
||||
displaySetInstanceUID,
|
||||
tabs,
|
||||
activeTabName
|
||||
);
|
||||
if (!thumbnailLocation) {
|
||||
return;
|
||||
}
|
||||
@@ -489,7 +494,7 @@ function _mapDisplaySets(displaySets, displaySetLoadingState, thumbnailImageSrcM
|
||||
seriesNumber: ds.SeriesNumber,
|
||||
modality: ds.Modality,
|
||||
seriesDate: formatDate(ds.SeriesDate),
|
||||
numInstances: ds.numImageFrames,
|
||||
numInstances: ds.numImageFrames ?? ds.instances?.length,
|
||||
loadingProgress,
|
||||
countIcon: ds.countIcon,
|
||||
messages: ds.messages,
|
||||
@@ -538,11 +543,13 @@ function _findTabAndStudyOfDisplaySet(
|
||||
tabs: TabsProps,
|
||||
currentTabName: string
|
||||
) {
|
||||
const current = tabs.find(tab => tab.name===currentTabName) || tabs[0];
|
||||
const current = tabs.find(tab => tab.name === currentTabName) || tabs[0];
|
||||
const biasedTabs = [current, ...tabs];
|
||||
|
||||
for (let t = 0; t < biasedTabs.length; t++) {
|
||||
const study = biasedTabs[t].studies.find(study => study.displaySets.find(ds => ds.displaySetInstanceUID ===displaySetInstanceUID));
|
||||
const study = biasedTabs[t].studies.find(study =>
|
||||
study.displaySets.find(ds => ds.displaySetInstanceUID === displaySetInstanceUID)
|
||||
);
|
||||
if (study) {
|
||||
return {
|
||||
tabName: biasedTabs[t].name,
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
import PROMPT_RESPONSES from '../utils/_shared/PROMPT_RESPONSES';
|
||||
|
||||
/**
|
||||
* Creates and shows a report dialog prompt.
|
||||
* The input for this is:
|
||||
* - `title` shown in the dialog
|
||||
* - `modality` being stored, used to query existing series
|
||||
* - `minSeriesNumber` is the start of new series of this modality type.
|
||||
* Will get set to 4000 if not determined by the modality
|
||||
* - predecessorImageId is the image id that this series was currently loaded
|
||||
* from. That allows defaulting the dialog to show the specified series instead
|
||||
* of always creating a new series.
|
||||
*
|
||||
* The response is:
|
||||
* - `value`, the default name of the object/series being created
|
||||
* - `dataSourceName`, where to store the object to
|
||||
* - `series`, is the series to store do, as referenced by a predecessorImageId value.
|
||||
* - `priorSeriesNumber` is the previously lowest series number at least minSeriesNumber
|
||||
* of all the seris of the given modality type.
|
||||
*
|
||||
* This should be provided to the DICOM encoder, which will get the predecessor
|
||||
* sequence from the metaData provider so that the saved series will replace
|
||||
* the existing instance in the same series.
|
||||
* This will be falsy for a new series.
|
||||
*/
|
||||
export default function CreateReportDialogPrompt({
|
||||
title = 'Create Report',
|
||||
modality = 'SR',
|
||||
minSeriesNumber = 0,
|
||||
predecessorImageId,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}): Promise<{
|
||||
value: string;
|
||||
dataSourceName: string;
|
||||
priorSeriesNumber?: number;
|
||||
series: string;
|
||||
action: (typeof PROMPT_RESPONSES)[keyof typeof PROMPT_RESPONSES];
|
||||
}> {
|
||||
@@ -16,18 +43,33 @@ export default function CreateReportDialogPrompt({
|
||||
|
||||
const allowMultipleDataSources = window.config.allowMultiSelectExport;
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
minSeriesNumber ||=
|
||||
(modality === 'SR' && 3000) ||
|
||||
(modality === 'SEG' && 3100) ||
|
||||
(modality === 'RTSTRUCT' && 3200) ||
|
||||
4000;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
uiDialogService.show({
|
||||
id: 'report-dialog',
|
||||
title,
|
||||
content: ReportDialog,
|
||||
contentProps: {
|
||||
dataSources: allowMultipleDataSources ? dataSources : undefined,
|
||||
onSave: async ({ reportName, dataSource: selectedDataSource, series }) => {
|
||||
predecessorImageId,
|
||||
minSeriesNumber,
|
||||
modality,
|
||||
onSave: async ({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
series,
|
||||
priorSeriesNumber,
|
||||
}) => {
|
||||
resolve({
|
||||
value: reportName,
|
||||
dataSourceName: selectedDataSource,
|
||||
series,
|
||||
priorSeriesNumber,
|
||||
action: PROMPT_RESPONSES.CREATE_REPORT,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -11,28 +11,44 @@ type DataSource = {
|
||||
|
||||
type ReportDialogProps = {
|
||||
dataSources: DataSource[];
|
||||
modality?: string;
|
||||
predecessorImageId?: string;
|
||||
hide: () => void;
|
||||
onSave: (data: { reportName: string; dataSource: string | null; series: string | null }) => void;
|
||||
onSave: (data: {
|
||||
reportName: string;
|
||||
dataSource: string | null;
|
||||
series: string | null;
|
||||
priorSeriesNumber: number;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps) {
|
||||
function ReportDialog({
|
||||
dataSources,
|
||||
modality = 'SR',
|
||||
predecessorImageId,
|
||||
minSeriesNumber = 3000,
|
||||
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 [selectedSeries, setSelectedSeries] = useState<string | null>(predecessorImageId || null);
|
||||
const [reportName, setReportName] = useState('');
|
||||
|
||||
const seriesOptions = useMemo(() => {
|
||||
const displaySetsMap = displaySetService.getDisplaySetCache();
|
||||
const displaySets = Array.from(displaySetsMap.values());
|
||||
const options = displaySets
|
||||
.filter(ds => ds.Modality === 'SR')
|
||||
.filter(ds => ds.Modality === modality)
|
||||
.map(ds => ({
|
||||
value: ds.SeriesInstanceUID,
|
||||
value: ds.predecessorImageId || ds.SeriesInstanceUID,
|
||||
seriesNumber: isFinite(ds.SeriesNumber) ? ds.SeriesNumber : minSeriesNumber,
|
||||
description: ds.SeriesDescription,
|
||||
label: `${ds.SeriesDescription} ${ds.SeriesDate}/${ds.SeriesTime} ${ds.SeriesNumber}`,
|
||||
}));
|
||||
@@ -41,11 +57,12 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
{
|
||||
value: null,
|
||||
description: null,
|
||||
seriesNumber: minSeriesNumber,
|
||||
label: 'Create new series',
|
||||
},
|
||||
...options,
|
||||
];
|
||||
}, [displaySetService]);
|
||||
}, [displaySetService, modality]);
|
||||
|
||||
useEffect(() => {
|
||||
const seriesOption = seriesOptions.find(s => s.value === selectedSeries);
|
||||
@@ -58,6 +75,7 @@ function ReportDialog({ dataSources, hide, onSave, onCancel }: ReportDialogProps
|
||||
onSave({
|
||||
reportName,
|
||||
dataSource: selectedDataSource,
|
||||
priorSeriesNumber: Math.max(...seriesOptions.map(it => it.seriesNumber)),
|
||||
series: selectedSeries,
|
||||
});
|
||||
hide();
|
||||
|
||||
@@ -32,7 +32,8 @@ export default function getDisplaySetsFromUnsupportedSeries(instances) {
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
SeriesDescription: instance.SeriesDescription || '',
|
||||
Modality: instance.Modality,
|
||||
numImageFrames: instances.length,
|
||||
instances,
|
||||
instance: instances[instance.length - 1],
|
||||
unsupported: true,
|
||||
SOPClassHandlerId: 'unsupported',
|
||||
isReconstructable: false,
|
||||
|
||||
@@ -3,8 +3,6 @@ import { utils } from '@ohif/core';
|
||||
import createReportAsync from '../Actions/createReportAsync';
|
||||
import { createReportDialogPrompt } from '../Panels';
|
||||
import PROMPT_RESPONSES from './_shared/PROMPT_RESPONSES';
|
||||
import { getSRSeriesAndInstanceNumber } from './getSRSeriesAndInstanceNumber';
|
||||
import { getSeriesDateTime } from './getCurrentDicomDateTime';
|
||||
|
||||
const { filterAnd, filterMeasurementsByStudyUID, filterMeasurementsBySeriesUID } =
|
||||
utils.MeasurementFilters;
|
||||
@@ -27,9 +25,14 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
} = ctx;
|
||||
let displaySetInstanceUIDs;
|
||||
|
||||
const measurementData = measurementService.getMeasurements(measurementFilter);
|
||||
const predecessorImageId = findPredecessorImageId(measurementData);
|
||||
|
||||
try {
|
||||
const promptResult = await createReportDialogPrompt({
|
||||
title: defaultSaveTitle,
|
||||
predecessorImageId,
|
||||
minSeriesNumber: 3000,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
});
|
||||
@@ -37,16 +40,9 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
if (promptResult.action === PROMPT_RESPONSES.CREATE_REPORT) {
|
||||
const dataSources = extensionManager.getDataSources(promptResult.dataSourceName);
|
||||
const dataSource = dataSources[0];
|
||||
const measurementData = measurementService.getMeasurements(measurementFilter);
|
||||
|
||||
const SeriesDescription = promptResult.value || defaultSaveTitle;
|
||||
|
||||
const { SeriesNumber, InstanceNumber, referenceDisplaySet } = getSRSeriesAndInstanceNumber({
|
||||
displaySetService,
|
||||
SeriesInstanceUid: promptResult.series,
|
||||
});
|
||||
|
||||
const { SeriesDate, SeriesTime } = referenceDisplaySet ?? getSeriesDateTime();
|
||||
const { series, priorSeriesNumber, value: reportName } = promptResult;
|
||||
const SeriesDescription = reportName || defaultSaveTitle;
|
||||
|
||||
const getReport = async () => {
|
||||
return commandsManager.runCommand(
|
||||
@@ -57,11 +53,8 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
additionalFindingTypes: ['ArrowAnnotate'],
|
||||
options: {
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
SeriesInstanceUID: promptResult.series,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber: 1 + priorSeriesNumber,
|
||||
predecessorImageId: series,
|
||||
},
|
||||
},
|
||||
'CORNERSTONE_STRUCTURED_REPORT'
|
||||
@@ -90,4 +83,20 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
|
||||
}
|
||||
}
|
||||
|
||||
export function findPredecessorImageId(annotations) {
|
||||
let predecessorImageId;
|
||||
for (const annotation of annotations) {
|
||||
if (
|
||||
predecessorImageId &&
|
||||
annotation.predecessorImageId &&
|
||||
annotation.predecessorImageId !== predecessorImageId
|
||||
) {
|
||||
console.warn('Found multiple source predecessors, not defaulting to same series');
|
||||
return;
|
||||
}
|
||||
predecessorImageId ||= annotation.predecessorImageId;
|
||||
}
|
||||
return predecessorImageId;
|
||||
}
|
||||
|
||||
export default promptSaveReport;
|
||||
Reference in new issue
Block a user