Skip to content

Commit

Permalink
fix(staticwado): SM and RT and update the server with new data (#3422)
Browse files Browse the repository at this point in the history
* fix bugs for the RT for the new demo

* fix SM with the static-wado server

* remove thumbnail from tmtv

* migration guide

* fix stability for the rt struct

* apply review comments

* add loading indicator to SM

* pdf works

* try to fix relative bulkData

* fix the rest

* fix preflight for the SM

* fix typo

* apply review comments

* yarn lock
  • Loading branch information
sedghi authored May 29, 2023
1 parent b684d80 commit c7bcf11
Show file tree
Hide file tree
Showing 30 changed files with 293 additions and 159 deletions.
107 changes: 60 additions & 47 deletions extensions/cornerstone-dicom-rt/src/loadRTStruct.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ async function checkAndLoadContourData(instance, datasource) {
return Promise.reject('Invalid instance object or ROIContourSequence');
}

const promises = [];
let counter = 0;
const promisesMap = new Map();

for (const ROIContour of instance.ROIContourSequence) {
const referencedROINumber = ROIContour.ReferencedROINumber;
if (!ROIContour || !ROIContour.ContourSequence) {
return Promise.reject('Invalid ROIContour or ContourSequence');
promisesMap.set(referencedROINumber, [Promise.resolve([])]);
continue;
}

for (const Contour of ROIContour.ContourSequence) {
Expand All @@ -21,9 +22,15 @@ async function checkAndLoadContourData(instance, datasource) {
}

const contourData = Contour.ContourData;
counter++;

if (Array.isArray(contourData)) {
promises.push(Promise.resolve(contourData));
promisesMap.has(referencedROINumber)
? promisesMap
.get(referencedROINumber)
.push(Promise.resolve(contourData))
: promisesMap.set(referencedROINumber, [
Promise.resolve(contourData),
]);
} else if (contourData && contourData.BulkDataURI) {
const bulkDataURI = contourData.BulkDataURI;

Expand All @@ -44,53 +51,59 @@ async function checkAndLoadContourData(instance, datasource) {
SOPInstanceUID: instance.SOPInstanceUID,
});

promises.push(bulkDataPromise);
promisesMap.has(referencedROINumber)
? promisesMap.get(referencedROINumber).push(bulkDataPromise)
: promisesMap.set(referencedROINumber, [bulkDataPromise]);
} else {
return Promise.reject(`Invalid ContourData: ${contourData}`);
}
}
}
const flattenedPromises = promises.flat();
const resolvedPromises = await Promise.allSettled(flattenedPromises);

// Modify contourData and replace it in its corresponding ROIContourSequence's Contour's contourData
let index = 0;
instance.ROIContourSequence.forEach((ROIContour, roiIndex) => {
ROIContour.ContourSequence.forEach((Contour, contourIndex) => {
const promise = resolvedPromises[index++];

if (promise.status === 'fulfilled') {
const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array);
if (
typeof dataUint8Array === 'string' &&
dataUint8Array.includes('\\')
) {
const numSlashes = (dataUint8Array.match(/\\/g) || []).length;
let startIndex = 0;
let endIndex = dataUint8Array.indexOf('\\', startIndex);
let numbersParsed = 0;
const ContourData = [];

while (numbersParsed !== numSlashes + 1) {
const str = dataUint8Array.substring(startIndex, endIndex);
let value = parseFloat(str);

ContourData.push(value);
startIndex = endIndex + 1;
endIndex = dataUint8Array.indexOf('\\', startIndex);
endIndex === -1 ? (endIndex = dataUint8Array.length) : endIndex;
numbersParsed++;

const resolvedPromisesMap = new Map();
for (const [key, promiseArray] of promisesMap.entries()) {
resolvedPromisesMap.set(key, await Promise.allSettled(promiseArray));
}

instance.ROIContourSequence.forEach(ROIContour => {
try {
const referencedROINumber = ROIContour.ReferencedROINumber;
const resolvedPromises = resolvedPromisesMap.get(referencedROINumber);

if (ROIContour.ContourSequence) {
ROIContour.ContourSequence.forEach((Contour, index) => {
const promise = resolvedPromises[index];
if (promise.status === 'fulfilled') {
if (
Array.isArray(promise.value) &&
promise.value.every(Number.isFinite)
) {
// If promise.value is already an array of numbers, use it directly
Contour.ContourData = promise.value;
} else {
// If the resolved promise value is a byte array (Blob), it needs to be decoded
const uint8Array = new Uint8Array(promise.value);
const textDecoder = new TextDecoder();
const dataUint8Array = textDecoder.decode(uint8Array);
if (
typeof dataUint8Array === 'string' &&
dataUint8Array.includes('\\')
) {
Contour.ContourData = dataUint8Array
.split('\\')
.map(parseFloat);
} else {
Contour.ContourData = [];
}
}
} else {
console.error(promise.reason);
}
Contour.ContourData = ContourData;
} else {
Contour.ContourData = [];
}
} else {
console.error(promise.reason);
});
}
});
} catch (error) {
console.error(error);
}
});
}

Expand All @@ -104,7 +117,7 @@ export default async function loadRTStruct(
'@ohif/extension-cornerstone.utilityModule.common'
);
const dataSource = extensionManager.getActiveDataSource()[0];
const { useBulkDataURI } = dataSource.getConfig?.() || {};
const { bulkDataURI } = dataSource.getConfig?.() || {};

const { dicomLoaderService } = utilityModule.exports;
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
Expand All @@ -116,7 +129,7 @@ export default async function loadRTStruct(
rtStructDisplaySet.isLoaded = true;
let instance = rtStructDisplaySet.instance;

if (!useBulkDataURI) {
if (!bulkDataURI || !bulkDataURI.enabled) {
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
rtStructDisplaySet,
null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ContextMenuController from './ContextMenuController';
import * as ContextMenuItemsBuilder from './ContextMenuItemsBuilder';
import defaultContextMenu from './defaultContextMenu';
import * as CustomizeableContextMenuTypes from './types';
import * as CustomizableContextMenuTypes from './types';

export {
ContextMenuController,
CustomizeableContextMenuTypes,
CustomizableContextMenuTypes,
ContextMenuItemsBuilder,
defaultContextMenu,
};
11 changes: 11 additions & 0 deletions extensions/default/src/DicomWebDataSource/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './retrieveStudyMetadata.js';
import StaticWadoClient from './utils/StaticWadoClient';
import getDirectURL from '../utils/getDirectURL';
import { fixBulkDataURI } from './utils/fixBulkDataURI';

const { DicomMetaDictionary, DicomDict } = dcmjs.data;

Expand Down Expand Up @@ -373,13 +374,23 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) {
*/
const addRetrieveBulkData = instance => {
const naturalized = naturalizeDataset(instance);

// if we konw the server doesn't use bulkDataURI, then don't
if (!dicomWebConfig.bulkDataURI?.enabled) {
return naturalized;
}

Object.keys(naturalized).forEach(key => {
const value = naturalized[key];

// The value.Value will be set with the bulkdata read value
// in which case it isn't necessary to re-read this.
if (value && value.BulkDataURI && !value.Value) {
// Provide a method to fetch bulkdata
value.retrieveBulkData = () => {
// handle the scenarios where bulkDataURI is relative path
fixBulkDataURI(value, naturalized, dicomWebConfig);

const options = {
// The bulkdata fetches work with either multipart or
// singlepart, so set multipart to false to let the server
Expand Down
56 changes: 56 additions & 0 deletions extensions/default/src/DicomWebDataSource/utils/fixBulkDataURI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Modifies a bulkDataURI to ensure it is absolute based on the DICOMWeb configuration and
* instance data. The modification is in-place.
*
* If the bulkDataURI is relative to the series or study (according to the DICOM standard),
* it is made absolute by prepending the relevant paths.
*
* In scenarios where the bulkDataURI is a server-relative path (starting with '/'), the function
* handles two cases:
*
* 1. If the wado root is absolute (starts with 'http'), it prepends the wado root to the bulkDataURI.
* 2. If the wado root is relative, no changes are needed as the bulkDataURI is already correctly relative to the server root.
*
* @param value - The object containing BulkDataURI to be fixed.
* @param instance - The object (DICOM instance data) containing StudyInstanceUID and SeriesInstanceUID.
* @param dicomWebConfig - The DICOMWeb configuration object, containing wadoRoot and potentially bulkDataURI.relativeResolution.
* @returns The function modifies `value` in-place, it does not return a value.
*/
function fixBulkDataURI(value, instance, dicomWebConfig) {
// in case of the relative path, make it absolute. The current DICOM standard says
// the bulkdataURI is relative to the series. However, there are situations where
// it can be relative to the study too
if (
!value.BulkDataURI.startsWith('http') &&
!value.BulkDataURI.startsWith('/')
) {
if (dicomWebConfig.bulkDataURI?.relativeResolution === 'studies') {
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${instance.StudyInstanceUID}/${value.BulkDataURI}`;
} else if (
dicomWebConfig.bulkDataURI?.relativeResolution === 'series' ||
!dicomWebConfig.bulkDataURI?.relativeResolution
) {
value.BulkDataURI = `${dicomWebConfig.wadoRoot}/studies/${instance.StudyInstanceUID}/series/${instance.SeriesInstanceUID}/${value.BulkDataURI}`;
}

return;
}

// in case it is relative path but starts at the server (e.g., /bulk/1e, note the missing http
// in the beginning and the first character is /) There are two scenarios, whether the wado root
// is absolute or relative. In case of absolute, we need to prepend the wado root to the bulkdata
// uri (e.g., bulkData: /bulk/1e, wado root: http://myserver.com/dicomweb, output: http://myserver.com/bulk/1e)
// and in case of relative wado root, we need to prepend the bulkdata uri to the wado root (e.g,. bulkData: /bulk/1e
// wado root: /dicomweb, output: /bulk/1e)
if (value.BulkDataURI[0] === '/') {
if (dicomWebConfig.wadoRoot.startsWith('http')) {
// Absolute wado root
const url = new URL(dicomWebConfig.wadoRoot);
value.BulkDataURI = `${url.origin}${value.BulkDataURI}`;
} else {
// Relative wado root, we don't need to do anything, bulkdata uri is already correct
}
}
}

export { fixBulkDataURI };
3 changes: 3 additions & 0 deletions extensions/default/src/DicomWebDataSource/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { fixBulkDataURI } from './fixBulkDataURI';

export { fixBulkDataURI };
4 changes: 2 additions & 2 deletions extensions/default/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { ServicesManager, utils, Types } from '@ohif/core';
import {
ContextMenuController,
defaultContextMenu,
} from './CustomizeableContextMenu';
} from './CustomizableContextMenu';
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
import reuseCachedLayouts from './utils/reuseCachedLayouts';
import findViewportsByPosition, {
findOrCreateViewport as layoutFindOrCreate,
} from './findViewportsByPosition';

import { ContextMenuProps } from './CustomizeableContextMenu/types';
import { ContextMenuProps } from './CustomizableContextMenu/types';
import { NavigateHistory } from './types/commandModuleTypes';
import { history } from '@ohif/viewer';

Expand Down
8 changes: 5 additions & 3 deletions extensions/default/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import { id } from './id.js';
import preRegistration from './init';
import {
ContextMenuController,
CustomizeableContextMenuTypes,
} from './CustomizeableContextMenu';
CustomizableContextMenuTypes,
} from './CustomizableContextMenu';
import * as dicomWebUtils from './DicomWebDataSource/utils';

const defaultExtension: Types.Extensions.Extension = {
/**
Expand Down Expand Up @@ -47,6 +48,7 @@ export default defaultExtension;

export {
ContextMenuController,
CustomizeableContextMenuTypes,
CustomizableContextMenuTypes,
getStudiesForPatientByMRN,
dicomWebUtils,
};
38 changes: 10 additions & 28 deletions extensions/default/src/utils/getDirectURL.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
DicomMetadataStore,
IWebApiDataSource,
utils,
errorHandler,
classes,
} from '@ohif/core';
import { utils } from '@ohif/core';

/**
* Generates a URL that can be used for direct retrieve of the bulkdata
Expand Down Expand Up @@ -57,31 +51,19 @@ const getDirectURL = (config, params) => {
const BulkDataURI =
(value && value.BulkDataURI) ||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
const hasQuery = BulkDataURI.indexOf('?') != -1;
const hasAccept = BulkDataURI.indexOf('accept=') != -1;
const hasQuery = BulkDataURI.indexOf('?') !== -1;
const hasAccept = BulkDataURI.indexOf('accept=') !== -1;
const acceptUri =
BulkDataURI +
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
if (BulkDataURI.indexOf('http') === 0) {
if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
} else {
return acceptUri;
}
}
if (BulkDataURI.indexOf('/') === 0) {
return wadoRoot + acceptUri;
}
if (BulkDataURI.indexOf('series/') == 0) {
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
}
if (BulkDataURI.indexOf('instances/') === 0) {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
}
if (BulkDataURI.indexOf('bulkdata/') === 0) {
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;

if (tag === 'PixelData' || tag === 'EncapsulatedDocument') {
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/instances/${SOPInstanceUID}/rendered`;
}
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);

// The DICOMweb standard states that the default is multipart related, and then
// separately states that the accept parameter is the URL parameter equivalent of the accept header.
return acceptUri;
};

export default getDirectURL;
Loading

0 comments on commit c7bcf11

Please sign in to comment.