Skip to content
Draft
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
7 changes: 4 additions & 3 deletions extensions/cornerstone/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export default async function init({
interaction: appConfig?.maxNumRequests?.interaction || 100,
thumbnail: appConfig?.maxNumRequests?.thumbnail || 75,
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
precache: appConfig?.maxNumRequests?.precache || 10,
};

initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
Expand Down Expand Up @@ -253,10 +254,10 @@ export default async function init({

/**
* Runs error handler for failed requests.
* @param event
* @param event
*/
const imageLoadFailedHandler = ({ detail }) => {
const handler = errorHandler.getHTTPErrorHandler()
const handler = errorHandler.getHTTPErrorHandler();
handler(detail.error);
};

Expand Down Expand Up @@ -290,7 +291,7 @@ export default async function init({
});
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_FAILED, imageLoadFailedHandler);
eventTarget.addEventListener(EVENTS.IMAGE_LOAD_ERROR, imageLoadFailedHandler);

function elementEnabledHandler(evt) {
const { element } = evt.detail;

Expand Down
11 changes: 1 addition & 10 deletions extensions/cornerstone/src/initWADOImageLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function initWADOImageLoader(
use16BitDataType:
Boolean(appConfig.useNorm16Texture) || Boolean(appConfig.preferSizeOverAccuracy),
},
beforeSend: function (xhr, imageId) {
beforeSend: function (xhr) {
//TODO should be removed in the future and request emitted by DicomWebDataSource
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
const headers = userAuthenticationService.getAuthorizationHeader();
Expand All @@ -70,15 +70,6 @@ export default function initWADOImageLoader(
Object.assign(xhrRequestHeaders, headers);
}

const instance = cornerstone.metaData.get('instance', imageId);
const { CustomOffsetTable, CustomOffsetTableLengths, FileOffsets } = instance;
if (FileOffsets && !(CustomOffsetTable && CustomOffsetTableLengths)) {
// A seperate logic is used if offset tables are present in cornerstone3D.
const { startByte, endByte } = instance.FileOffsets;
const rangeHeader = { Range: `bytes=${startByte}-${endByte}` };
Object.assign(xhrRequestHeaders, rangeHeader);
}

return xhrRequestHeaders;
},
errorInterceptor: error => {
Expand Down
3 changes: 2 additions & 1 deletion extensions/default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"@cornerstonejs/calculate-suv": "^1.1.0"
"@cornerstonejs/calculate-suv": "^1.1.0",
"cod-dicomweb-server": "^1.3.4"
}
}
235 changes: 235 additions & 0 deletions extensions/default/src/DicomWebDataSource/codDicomWebServerWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { CodDicomWebServer } from 'cod-dicomweb-server';

const Properties = {
StudyUID: '0020000D',
SeriesUID: '0020000E',
};

class CodDicomWebServerClient {
/**
* @param {Object} config
*/
constructor(config) {
this.baseURL = config.url;
this.qidoURL = this.baseURL;
this.wadoURL = this.baseURL;
this.config = config;
this.headers = config.headers;
this.errorInterceptor = config.errorInterceptor;

this._codServer = new CodDicomWebServer({ domain: parseDomainFromBaseURL(this.baseURL) });
this.deidStudyInstanceUIDMap = new Map(); // Map of study instance UIDs to deid study instance UIDs
}

/**
* @param {URLSearchParams} queryParams
*/
async fetchStudiesMetadata(queryParams) {
this._studiesMetadata = await this.filesFromStudyInstanceUID({
wadoURL: this.wadoURL,
bucketName: queryParams.get('bucket'),
prefix: queryParams.get('bucket-prefix'),
studyuids: queryParams.getAll('StudyInstanceUIDs'),
headers: this.headers,
})
.then(studies => {
return studies.filter(study => {
study.series = study.series.filter(aSeries => {
if (aSeries.instances.length) {
return true;
}

console.warn('No instance found in series ' + aSeries.deidSeriesInstanceUID);
return false;
});

if (study.series.length) {
const studyUID = study.series[0].instances[0]['0020000D'].Value[0];
this.deidStudyInstanceUIDMap.set(study.deidStudyInstanceUID, studyUID);
return true;
}

return false;
});
})
.catch(error => {
this.errorInterceptor(error);
return [];
});
}

/**
* @param {string} deidStudyInstanceUID
*/
getStudyUIDForDeidStudyUID(deidStudyInstanceUID) {
const studyWithDeidStudyUID = this._studiesMetadata.find(
study => (study.deidStudyInstanceUID = deidStudyInstanceUID)
);

return this._getProperty(studyWithDeidStudyUID, Properties.StudyUID);
}

/**
* @param {Object} data
* @param {string} property
*/
_getProperty(data, property) {
if (!data) {
return;
}

return (
data[property]?.Value[0] ||
data.instances?.[0][property].Value[0] ||
data.series?.[0].instances[0][property].Value[0]
);
}

/**
* @param {Object[]} studies
* @param {string} studyInstanceUID
*/
_findStudy(studies = [], studyInstanceUID) {
return studies.find(
study => this._getProperty(study, Properties.StudyUID) === studyInstanceUID
);
}

/**
* @param {Object[]} series
* @param {string} seriesInstanceUID
*/
_findSeries(series = [], seriesInstanceUID) {
return series.find(
series => this._getProperty(series, Properties.SeriesUID) === seriesInstanceUID
);
}

/**
* @param {Object} options
* @param {string} options.studyInstanceUID
* @param {string} options.seriesInstanceUID
*/
retrieveSeriesMetadata({ studyInstanceUID, seriesInstanceUID }) {
const studyFound = this._findStudy(this._studiesMetadata, studyInstanceUID);
const seriesFound = this._findSeries(studyFound?.series, seriesInstanceUID);

return new Promise((resolve, reject) => {
if (seriesFound) {
resolve(seriesFound.instances);
} else {
reject();
}
});
}

/**
* @param {Object} options
* @param {string} options.studyInstanceUID
*/
retrieveStudyMetadata({ studyInstanceUID }) {
const studyFound = this._findStudy(this._studiesMetadata, studyInstanceUID);

return new Promise((resolve, reject) => {
if (studyFound) {
resolve(studyFound.series.flatMap(aSeries => aSeries.instances));
} else {
reject();
}
});
}

/**
* @param {Object} options
* @param {string} options.studyInstanceUID
*/
searchForSeries({ studyInstanceUID }) {
const studyFound = this._findStudy(this._studiesMetadata, studyInstanceUID);

return new Promise(resolve => {
if (studyFound) {
resolve(studyFound.series.flatMap(aSeries => aSeries.instances));
} else {
resolve([]);
}
});
}

/**
* @param {Object} options
* @param {Object} options.queryParams
* @param {string} options.queryParams.StudyInstanceUID
*/
searchForStudies({ queryParams }) {
const studyFound = this._studiesMetadata.find(study => {
if (this._getProperty(study, Properties.StudyUID) === queryParams.StudyInstanceUID) {
return true;
}

const tagFound = Object.entries(queryParams).find(
([tag, value]) => this._getProperty(study, tag) === value
);
if (tagFound) {
return true;
}

return false;
});

return new Promise(resolve => {
if (studyFound) {
resolve([studyFound.series[0].instances[0]]);
} else {
resolve([]);
}
});
}

/**
* @param {Object} params
* @param {string} params.wadoURL
* @param {string} params.bucketName
* @param {string[]} params.studyuids
* @param {string} params.prefix
* @param {Object} params.headers
*/
async filesFromStudyInstanceUID({ wadoURL, bucketName, prefix, studyuids, headers }) {
const delimiter = '/';
const domain = parseDomainFromBaseURL(wadoURL);
const bucketComponents = wadoURL.split(domain + delimiter)[1].split(delimiter);
const bucket = bucketName || bucketComponents[0];
const bucketPrefix = prefix || bucketComponents.slice(1).join(delimiter) || 'dicomweb';
const urlRoot = `${domain}/${bucket}/${bucketPrefix}`;

const studyMetadata = studyuids.map(async (/** @type {string} */ deidStudyInstanceuid) => {
const folderPath = `${bucketPrefix}/studies/${deidStudyInstanceuid}/series/`;
const apiUrl = `${domain}/storage/v1/b/${bucket}/o?prefix=${folderPath}&delimiter=${delimiter}`;
const response = await fetch(apiUrl, { headers });
const res = await response.json();
const folders = res.prefixes || [];
const series = folders.map(async (/** @type {string} */ folderPath) => {
const deidSeriesInstanceUID = folderPath.split('/series/')[1].split(delimiter)[0];
const wadoUrl = `${urlRoot}/studies/${deidStudyInstanceuid}/series/${deidSeriesInstanceUID}/metadata`;
return {
deidSeriesInstanceUID,
instances: await this._codServer.fetchCod(wadoUrl, headers),
};
});
return Promise.all(series).then(result => ({
deidStudyInstanceUID: deidStudyInstanceuid,
series: result,
}));
});
return await Promise.all(studyMetadata);
}
}

/**
* @param {string} baseRoot
*/
function parseDomainFromBaseURL(baseRoot) {
const [firstPart, secondPart] = baseRoot.split('://');
return `${firstPart}://${secondPart.split('/')[0]}`;
}

export default CodDicomWebServerClient;
25 changes: 25 additions & 0 deletions extensions/default/src/DicomWebDataSource/getCodImageId.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import getWADORSImageId from './utils/getWADORSImageId';

/**
* @param {Object} params
* @param {Object} params.instance
* @param {number} [params.frame]
* @param {Object} params.config
*/
export default function getCodImageId({ instance, frame, config }) {
if (!instance) {
return;
}

let wadoRsImageId;

if (instance.imageId && frame === undefined) {
wadoRsImageId = instance.imageId;
} else if (instance.url) {
wadoRsImageId = instance.url;
} else {
wadoRsImageId = getWADORSImageId(instance, config, frame);
}

return wadoRsImageId.replace('wadors:', 'cod:');
}
Loading