Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 98 additions & 51 deletions extensions/default/src/DicomJSONDataSource/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,86 @@ function wrapSequences(obj) {
Array.isArray(obj) ? [] : {}
);
}

function hasFrameQuery(url) {
return /[?&]frame=/.test(url);
}

function getDicomJSONImageId({ instance, frame, config }) {
if (!instance) {
return;
}

if (!instance.url) {
return getImageId({ instance, frame, config });
}

const imageId = getImageId({ instance, config });

if (frame === undefined || !imageId || hasFrameQuery(imageId)) {
return imageId;
}

return `${imageId}&frame=${frame}`;
}

function getInstanceMetadata({ instance, series, study, imageId }) {
const { instances, NumberOfFrames, ...seriesMetadata } = series;
const modifiedMetadata = wrapSequences(instance.metadata);
const obj = {
...modifiedMetadata,
url: instance.url,
imageId,
...seriesMetadata,
...study,
};

if (modifiedMetadata.NumberOfFrames === undefined && instances.length === 1 && NumberOfFrames) {
obj.NumberOfFrames = NumberOfFrames;
}

delete obj.instances;
delete obj.series;
return obj;
}

function getDicomJSONImageIdsForDisplaySet({ displaySet, seriesInstances, config }) {
const images = displaySet.images;
const imageIds = [];

if (!images) {
return imageIds;
}

const instanceMap = new Map();
if (seriesInstances) {
seriesInstances.forEach(instance => {
if (instance?.metadata?.SOPInstanceUID) {
const { metadata, url } = instance;
const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || [];
existingInstances.push({ ...metadata, url });
instanceMap.set(metadata.SOPInstanceUID, existingInstances);
}
});
}

images.forEach(instance => {
const NumberOfFrames = instance.NumberOfFrames || 1;
const instances =
NumberOfFrames > 1 ? instanceMap.get(instance.SOPInstanceUID) || [instance] : [instance];
for (let i = 0; i < NumberOfFrames; i++) {
const imageId = getDicomJSONImageId({
instance: instances[Math.min(i, instances.length - 1)],
frame: NumberOfFrames > 1 ? i : undefined,
config,
});
imageIds.push(imageId);
}
});

return imageIds;
}

const getMetaDataByURL = url => {
return _store.urls.find(metaData => metaData.url === url);
};
Expand Down Expand Up @@ -94,7 +174,7 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {

series.instances.forEach(instance => {
const { metadata: naturalizedDicom } = instance;
const imageId = getImageId({ instance, config: dicomJsonConfig });
const imageId = getDicomJSONImageId({ instance, config: dicomJsonConfig });

const { query } = qs.parseUrl(instance.url);

Expand Down Expand Up @@ -227,22 +307,12 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
const numberOfSeries = series.length;
series.forEach((series, index) => {
const instances = series.instances.map(instance => {
// for instance.metadata if the key ends with sequence then
// we need to add a proxy to the first item in the sequence
// so that we can access the value of the sequence
// by using sequenceName.value
const modifiedMetadata = wrapSequences(instance.metadata);

const obj = {
...modifiedMetadata,
url: instance.url,
imageId: getImageId({ instance, config: dicomJsonConfig }),
...series,
...study,
};
delete obj.instances;
delete obj.series;
return obj;
return getInstanceMetadata({
instance,
series,
study,
imageId: getDicomJSONImageId({ instance, config: dicomJsonConfig }),
});
});
storeInstances(instances);
if (index === numberOfSeries - 1) {
Expand All @@ -260,46 +330,18 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
reject: {},
deleteStudyMetadataPromise: () => {},
getImageIdsForDisplaySet(displaySet) {
const images = displaySet.images;
const imageIds = [];

if (!images) {
return imageIds;
}

const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
const series = study.series.find(s => s.SeriesInstanceUID === SeriesInstanceUID) || {};

const instanceMap = new Map();
if (series.instances) {
series.instances.forEach(instance => {
if (instance?.metadata?.SOPInstanceUID) {
const { metadata, url } = instance;
const existingInstances = instanceMap.get(metadata.SOPInstanceUID) || [];
existingInstances.push({ ...metadata, url });
instanceMap.set(metadata.SOPInstanceUID, existingInstances);
}
});
}

displaySet.images.forEach(instance => {
const NumberOfFrames = instance.NumberOfFrames || 1;
const instances = instanceMap.get(instance.SOPInstanceUID) || [instance];
for (let i = 0; i < NumberOfFrames; i++) {
const imageId = getImageId({
instance: instances[Math.min(i, instances.length - 1)],
frame: NumberOfFrames > 1 ? i : undefined,
config: dicomJsonConfig,
});
imageIds.push(imageId);
}
return getDicomJSONImageIdsForDisplaySet({
displaySet,
seriesInstances: series.instances,
config: dicomJsonConfig,
});

return imageIds;
},
getImageIdsForInstance({ instance, frame }) {
const imageIds = getImageId({ instance, frame });
const imageIds = getDicomJSONImageId({ instance, frame });
return imageIds;
},
getStudyInstanceUIDs: ({ params, query }) => {
Expand All @@ -323,4 +365,9 @@ function createDicomJSONApi(dicomJsonConfig, servicesManager) {
return IWebApiDataSource.create(implementation);
}

export { createDicomJSONApi };
export {
createDicomJSONApi,
getDicomJSONImageId,
getDicomJSONImageIdsForDisplaySet,
getInstanceMetadata,
};
174 changes: 174 additions & 0 deletions extensions/default/src/DicomJSONDataSource/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
getDicomJSONImageId,
getDicomJSONImageIdsForDisplaySet,
getInstanceMetadata,
} from './index';

jest.mock('@ohif/core', () => ({
DicomMetadataStore: {},
IWebApiDataSource: {
create: jest.fn(implementation => implementation),
},
classes: {
MetadataProvider: {
addImageIdToUIDs: jest.fn(),
},
},
utils: {
addAccessors: jest.fn(sequence => sequence),
},
}));

describe('DicomJSONDataSource helpers', () => {
const study = {
StudyInstanceUID: 'study-uid',
PatientID: 'patient-id',
};

const baseSeries = {
SeriesInstanceUID: 'series-uid',
Modality: 'XA',
SeriesNumber: 3,
};

const baseMetadata = {
StudyInstanceUID: 'study-uid',
SeriesInstanceUID: 'series-uid',
SOPInstanceUID: 'sop-uid',
SOPClassUID: '1.2.840.10008.5.1.4.1.1.12.1',
Rows: 512,
Columns: 512,
};

it('keeps per-frame DICOM JSON instances as single-frame instances', () => {
const instances = [
{
metadata: { ...baseMetadata, InstanceNumber: 1 },
url: 'wadouri:https://example.com/xray.dcm&frame=1',
},
{
metadata: { ...baseMetadata, InstanceNumber: 2 },
url: 'wadouri:https://example.com/xray.dcm&frame=2',
},
];
const series = {
...baseSeries,
NumberOfFrames: 2,
instances,
};

const first = getInstanceMetadata({
instance: instances[0],
series,
study,
imageId: instances[0].url,
});
const second = getInstanceMetadata({
instance: instances[1],
series,
study,
imageId: instances[1].url,
});

expect(first.NumberOfFrames).toBeUndefined();
expect(second.NumberOfFrames).toBeUndefined();
expect(first.imageId).toBe('wadouri:https://example.com/xray.dcm&frame=1');
expect(second.imageId).toBe('wadouri:https://example.com/xray.dcm&frame=2');
});

it('preserves series-level NumberOfFrames for a single multiframe instance', () => {
const instance = {
metadata: baseMetadata,
url: 'wadouri:https://example.com/xray.dcm',
};
const series = {
...baseSeries,
NumberOfFrames: 2,
instances: [instance],
};

const result = getInstanceMetadata({
instance,
series,
study,
imageId: instance.url,
});

expect(result.NumberOfFrames).toBe(2);
});

it('appends a frame query to DICOM JSON instance URLs only when needed', () => {
expect(
getDicomJSONImageId({
instance: { url: 'wadouri:https://example.com/xray.dcm' },
frame: 0,
})
).toBe('wadouri:https://example.com/xray.dcm&frame=0');

expect(
getDicomJSONImageId({
instance: { url: 'wadouri:https://example.com/xray.dcm&frame=1' },
frame: 0,
})
).toBe('wadouri:https://example.com/xray.dcm&frame=1');
});

it('keeps generated WADO imageIds working when no DICOM JSON url is present', () => {
expect(
getDicomJSONImageId({
instance: baseMetadata,
frame: 1,
config: {
wadoUriRoot: 'https://example.com/wado',
},
})
).toContain('&frame=1');
});

it('returns the current per-frame URL when DICOM JSON repeats a SOPInstanceUID', () => {
const seriesInstances = [
{
metadata: { ...baseMetadata, InstanceNumber: 1 },
url: 'wadouri:https://example.com/xray.dcm&frame=1',
},
{
metadata: { ...baseMetadata, InstanceNumber: 2 },
url: 'wadouri:https://example.com/xray.dcm&frame=2',
},
];
const displaySet = {
images: [
{ ...baseMetadata, InstanceNumber: 1, url: seriesInstances[0].url },
{ ...baseMetadata, InstanceNumber: 2, url: seriesInstances[1].url },
],
};

expect(getDicomJSONImageIdsForDisplaySet({ displaySet, seriesInstances })).toEqual([
'wadouri:https://example.com/xray.dcm&frame=1',
'wadouri:https://example.com/xray.dcm&frame=2',
]);
});

it('expands a single DICOM JSON multiframe instance into frame imageIds', () => {
const seriesInstances = [
{
metadata: baseMetadata,
url: 'wadouri:https://example.com/xray.dcm',
},
];
const displaySet = {
images: [
{
...baseMetadata,
NumberOfFrames: 2,
url: seriesInstances[0].url,
},
],
};

expect(getDicomJSONImageIdsForDisplaySet({ displaySet, seriesInstances })).toEqual([
'wadouri:https://example.com/xray.dcm&frame=0',
'wadouri:https://example.com/xray.dcm&frame=1',
]);
});
});