* Feat #1130 Adding support for downloading and zipping files @ This is a combination of 12 commits. - Feat #1130 Initial Commit - Feat #1130 Adding support for hierachical lists - Feat #1130 [WIP] Adding support for downloading and zipping files - Feat #1130 Adding support for hierarchical folder structure study, series, instances on generated zip file - Feat #1130 Adding unit tests and addressing code review feedbacks - Feat #1130 POC (adding module to support on progress handling) - Feat #1130 Improving logic from progress tracking utils - Feat #1130 Renaming module with progress tracking utils - Feat #1130 Beta version of Progress Tracking utils - Feat #1130 Adding unit tests for Progress Tracking Utils - Feat #1130 Documenting public methods from progress tracking utils module - Feat #1130 Adding support for download and zip progress reporting * Feat #1130 Addressing code review feedback * Feat #1130 Preventing multiple downloads from executing at once * Feat #1130 Improving error message for concurrent downloads * Remove downloadAndZipSeriesOnViewports Co-authored-by: James Petts <jamesapetts@gmail.com>
This commit is contained in:
parent
2412c08595
commit
29d4955980
8
extensions/dicom-p10-downloader/.webpack/webpack.dev.js
Normal file
8
extensions/dicom-p10-downloader/.webpack/webpack.dev.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
};
|
||||||
38
extensions/dicom-p10-downloader/.webpack/webpack.prod.js
Normal file
38
extensions/dicom-p10-downloader/.webpack/webpack.prod.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const merge = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.join(__dirname, './..');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
|
||||||
|
return merge(commonConfig, {
|
||||||
|
devtool: 'source-map',
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: ROOT_DIR,
|
||||||
|
library: 'OHIFExtDicomP10Downloader',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
libraryExport: 'default',
|
||||||
|
filename: pkg.main,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
4
extensions/dicom-p10-downloader/CHANGELOG.md
Normal file
4
extensions/dicom-p10-downloader/CHANGELOG.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
21
extensions/dicom-p10-downloader/LICENSE
Normal file
21
extensions/dicom-p10-downloader/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Open Health Imaging Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1
extensions/dicom-p10-downloader/README.md
Normal file
1
extensions/dicom-p10-downloader/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @ohif/extension-dicom-p10-downloader
|
||||||
39
extensions/dicom-p10-downloader/package.json
Normal file
39
extensions/dicom-p10-downloader/package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-dicom-p10-downloader",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "OHIF extension for downloading DICOM P10 files",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"main": "dist/index.umd.js",
|
||||||
|
"module": "src/index.js",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package": "yarn run build",
|
||||||
|
"prepublishOnly": "yarn run build",
|
||||||
|
"start": "yarn run dev"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "^2.6.0",
|
||||||
|
"dicom-parser": "^1.8.3",
|
||||||
|
"dicomweb-client": "^0.5.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"file-saver": "^2.0.2",
|
||||||
|
"jszip": "^3.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
105
extensions/dicom-p10-downloader/src/commandsModule.js
Normal file
105
extensions/dicom-p10-downloader/src/commandsModule.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import {
|
||||||
|
save,
|
||||||
|
getDicomWebClientFromContext,
|
||||||
|
getSOPInstanceReferenceFromActiveViewport,
|
||||||
|
getSOPInstanceReferencesFromViewports,
|
||||||
|
} from './utils';
|
||||||
|
import _downloadAndZip from './downloadAndZip';
|
||||||
|
|
||||||
|
const {
|
||||||
|
utils: {
|
||||||
|
Queue,
|
||||||
|
},
|
||||||
|
} = OHIF;
|
||||||
|
|
||||||
|
export function getCommands(context) {
|
||||||
|
const queue = new Queue(1);
|
||||||
|
const actions = {
|
||||||
|
/**
|
||||||
|
* @example Running this command using Commands Manager
|
||||||
|
* commandsManager.runCommand(
|
||||||
|
* 'downloadAndZip',
|
||||||
|
* {
|
||||||
|
* listOfUIDs: [...],
|
||||||
|
* options: {
|
||||||
|
* progress(status) {
|
||||||
|
* console.info('Progress:', (status.progress * 100).toFixed(2) + '%');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* 'VIEWER'
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
downloadAndZip({ servers, dicomWebClient, listOfUIDs, options }) {
|
||||||
|
return save(
|
||||||
|
_downloadAndZip(
|
||||||
|
dicomWebClient || getDicomWebClientFromContext(context, servers),
|
||||||
|
listOfUIDs,
|
||||||
|
options
|
||||||
|
),
|
||||||
|
listOfUIDs
|
||||||
|
);
|
||||||
|
},
|
||||||
|
downloadAndZipSeriesOnViewports({ servers, viewports, progress }) {
|
||||||
|
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||||
|
const listOfUIDs = getSOPInstanceReferencesFromViewports(viewports);
|
||||||
|
return save(
|
||||||
|
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
|
||||||
|
listOfUIDs
|
||||||
|
);
|
||||||
|
},
|
||||||
|
downloadAndZipSeriesOnActiveViewport({ servers, viewports, progress }) {
|
||||||
|
const dicomWebClient = getDicomWebClientFromContext(context, servers);
|
||||||
|
const listOfUIDs = getSOPInstanceReferenceFromActiveViewport(viewports);
|
||||||
|
return save(
|
||||||
|
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
|
||||||
|
listOfUIDs
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = {
|
||||||
|
downloadAndZip: {
|
||||||
|
commandFn: queue.bindSafe(actions.downloadAndZip, error),
|
||||||
|
storeContexts: ['servers'],
|
||||||
|
},
|
||||||
|
downloadAndZipSeriesOnViewports: {
|
||||||
|
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
|
||||||
|
storeContexts: ['servers', 'viewports'],
|
||||||
|
options: { progress },
|
||||||
|
},
|
||||||
|
downloadAndZipSeriesOnActiveViewport: {
|
||||||
|
commandFn: queue.bindSafe(
|
||||||
|
actions.downloadAndZipSeriesOnActiveViewport,
|
||||||
|
error
|
||||||
|
),
|
||||||
|
storeContexts: ['servers', 'viewports'],
|
||||||
|
options: { progress },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions,
|
||||||
|
definitions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
function progress(status) {
|
||||||
|
OHIF.log.info(
|
||||||
|
'Download and Zip Progress:',
|
||||||
|
(status.progress * 100.0).toFixed(2) + '%'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(e) {
|
||||||
|
if (e.message === 'Queue limit reached') {
|
||||||
|
OHIF.log.warn('A download is already in progress, please wait.');
|
||||||
|
} else {
|
||||||
|
OHIF.log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
244
extensions/dicom-p10-downloader/src/downloadAndZip.js
Normal file
244
extensions/dicom-p10-downloader/src/downloadAndZip.js
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import { api } from 'dicomweb-client';
|
||||||
|
import dicomParser from 'dicom-parser';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const {
|
||||||
|
utils: {
|
||||||
|
isDicomUid,
|
||||||
|
hierarchicalListUtils,
|
||||||
|
progressTrackingUtils: progressUtils,
|
||||||
|
},
|
||||||
|
} = OHIF;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download and Zip all DICOM P10 instances from specified DICOM Web Client
|
||||||
|
* based on an hierarchical list of UIDs;
|
||||||
|
*
|
||||||
|
* @param {DICOMwebClient} dicomWebClient A DICOMwebClient instance through
|
||||||
|
* which the referenced instances will be retrieved;
|
||||||
|
* @param {Array} listOfUIDs The hierarchical list of UIDs from the instances
|
||||||
|
* that should be retrieved:
|
||||||
|
* A hierarchical list of UIDs is a regular JS Array where the type of the UID
|
||||||
|
* (study, series, instance) is determined by its nasting lavel. For example:
|
||||||
|
* @ The following list instructs the library to download all the instances
|
||||||
|
* from both studies "A" and "B":
|
||||||
|
*
|
||||||
|
* ['studyUIDFromA', 'studyUIDFromB']
|
||||||
|
*
|
||||||
|
* @ In the previous example both UIDs are treated as STUDY UIDs because both
|
||||||
|
* of them are listed in the same (top) level of the list. If, on the other
|
||||||
|
* hand, only instances from series "I" and "J" from the study "B"
|
||||||
|
* are to be downloaded, the expected hierarchical list would be:
|
||||||
|
*
|
||||||
|
* ['studyUIDFromA', ['studyUIDFromB', ['seriesUIDFromI', 'seriesUIDFromJ']]]
|
||||||
|
*
|
||||||
|
* @ Which, when prettified, reads like this:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* 'studyUIDFromA',
|
||||||
|
* ['studyUIDFromB', [
|
||||||
|
* 'seriesUIDFromI',
|
||||||
|
* 'seriesUIDFromJ'
|
||||||
|
* ]]
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @ Furthermore, if only instances "X", "Y" and "Z" from series "J" need to
|
||||||
|
* be downloaded (instead of all the instances from that series), the list
|
||||||
|
* could be changed to:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* 'studyUIDFromA',
|
||||||
|
* ['studyUIDFromB', [
|
||||||
|
* 'seriesUIDFromI',
|
||||||
|
* ['seriesUIDFromJ', [
|
||||||
|
* 'instanceUIDFromX',
|
||||||
|
* 'instanceUIDFromY',
|
||||||
|
* 'instanceUIDFromZ'
|
||||||
|
* ]]
|
||||||
|
* ]]
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* Please refer to hierarchicalListUtils.js for more information and utilities;
|
||||||
|
*
|
||||||
|
* @param {Object} options A plain object with options;
|
||||||
|
* @param {function} options.progress A callback to retrieve notifications
|
||||||
|
* @returns {Promise} A promise that resolves to an URL from which the ZIP file
|
||||||
|
* can be downloaded;
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function downloadAndZip(dicomWebClient, listOfUIDs, options) {
|
||||||
|
if (dicomWebClient instanceof api.DICOMwebClient) {
|
||||||
|
const settings = buildSettings(listOfUIDs, options);
|
||||||
|
const { compression } = settings.tasks;
|
||||||
|
// Register user-provided progress handler as a task list observer
|
||||||
|
progressUtils.addObserver(settings.taskList, settings.options.progress);
|
||||||
|
const buffers = await downloadAll(dicomWebClient, settings).catch(error => {
|
||||||
|
// Reject promise from compression task on download failure
|
||||||
|
compression.deferred.reject(error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
compression.deferred.resolve(zipAll(buffers, settings));
|
||||||
|
const url = await compression.deferred.promise;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
throw new Error('A valid DICOM Web Client instance is expected');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function zipAll(buffers, settings) {
|
||||||
|
const zip = new JSZip();
|
||||||
|
OHIF.log.info('Adding DICOM P10 files to archive:', buffers.length);
|
||||||
|
buffers.forEach((buffer, i) => {
|
||||||
|
const path = buildPath(buffer) || `${i}.dcm`;
|
||||||
|
zip.file(path, buffer);
|
||||||
|
});
|
||||||
|
// Set compression task progress to 50%
|
||||||
|
progressUtils.update(settings.tasks.compression.task, 0.5);
|
||||||
|
const blob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSettings(listOfUIDs, options) {
|
||||||
|
const taskList = progressUtils.createList();
|
||||||
|
const compression = progressUtils.addDeferred(taskList);
|
||||||
|
const downloads = [];
|
||||||
|
|
||||||
|
// Build downloads list
|
||||||
|
hierarchicalListUtils.forEach(
|
||||||
|
listOfUIDs,
|
||||||
|
(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) => {
|
||||||
|
if (isDicomUid(StudyInstanceUID)) {
|
||||||
|
downloads.push({
|
||||||
|
tracking: progressUtils.addDeferred(taskList),
|
||||||
|
parameters: [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Print tree of hierarchical references
|
||||||
|
OHIF.log.info('Downloading DICOM P10 files for references:');
|
||||||
|
OHIF.log.info(hierarchicalListUtils.print(listOfUIDs));
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: Object(options),
|
||||||
|
taskList,
|
||||||
|
tasks: {
|
||||||
|
downloads,
|
||||||
|
compression,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPath(buffer) {
|
||||||
|
let path;
|
||||||
|
try {
|
||||||
|
const byteArray = new Uint8Array(buffer);
|
||||||
|
const dataSet = dicomParser.parseDicom(byteArray, {
|
||||||
|
// Stop parsing after SeriesInstanceUID is found
|
||||||
|
untilTag: 'x0020000e',
|
||||||
|
});
|
||||||
|
const StudyInstanceUID = dataSet.string('x0020000d');
|
||||||
|
const SeriesInstanceUID = dataSet.string('x0020000e');
|
||||||
|
const SOPInstanceUID = dataSet.string('x00080018');
|
||||||
|
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
|
||||||
|
path = `${StudyInstanceUID}/${SeriesInstanceUID}/${SOPInstanceUID}.dcm`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
OHIF.log.error('Error parsing downloaded DICOM P10 file...', e);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadAll(dicomWebClient, settings) {
|
||||||
|
const { downloads } = settings.tasks;
|
||||||
|
|
||||||
|
// Make sure at least one download was initiated
|
||||||
|
if (downloads.length < 1) {
|
||||||
|
throw new Error('No valid reference to be downloaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = downloads.map(item => {
|
||||||
|
const {
|
||||||
|
parameters,
|
||||||
|
tracking: { deferred, task },
|
||||||
|
} = item;
|
||||||
|
deferred.resolve(download(task, dicomWebClient, ...parameters));
|
||||||
|
return deferred.promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait on created download promises
|
||||||
|
return Promise.all(promises).then(results => {
|
||||||
|
const buffers = [];
|
||||||
|
// The "results" array may directly contain buffers (ArrayBuffer instances)
|
||||||
|
// or arrays of buffers, depending on the type of downloads initiated on the
|
||||||
|
// previous step (retrieveStudy, retrieveSeries or retrieveinstance). Ex:
|
||||||
|
// results = [buf1, [buf2, buf3], buf4, [buf5], ...];
|
||||||
|
results.forEach(
|
||||||
|
function select(nesting, result) {
|
||||||
|
if (result instanceof ArrayBuffer) {
|
||||||
|
buffers.push(result);
|
||||||
|
} else if (nesting && Array.isArray(result)) {
|
||||||
|
// "nesting" argument is important to make sure only two levels
|
||||||
|
// of arrays are visited. For example, "bufX" should not be visited:
|
||||||
|
// [buf1, [buf2, buf3, [bufX]], buf4, [buf5], ...];
|
||||||
|
result.forEach(select.bind(null, false));
|
||||||
|
}
|
||||||
|
}.bind(null, true)
|
||||||
|
);
|
||||||
|
return buffers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download(
|
||||||
|
task,
|
||||||
|
dicomWebClient,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
sopInstanceUID
|
||||||
|
) {
|
||||||
|
// Strict DICOM-formatted variable names COULDN'T be used here because the
|
||||||
|
// DICOM Web client interface expects them in this specific format.
|
||||||
|
// @TODO: Add support for download progress handler which will use the
|
||||||
|
// currently not use "task" param
|
||||||
|
if (!isDicomUid(studyInstanceUID)) {
|
||||||
|
throw new Error('Download requires at least a "StudyInstanceUID" property');
|
||||||
|
}
|
||||||
|
if (!isDicomUid(seriesInstanceUID)) {
|
||||||
|
// Download entire study
|
||||||
|
return dicomWebClient.retrieveStudy({
|
||||||
|
studyInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isDicomUid(sopInstanceUID)) {
|
||||||
|
// Download entire series
|
||||||
|
return dicomWebClient.retrieveSeries({
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Download specific instance
|
||||||
|
return dicomWebClient.retrieveInstance({
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
sopInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { downloadAndZip as default, downloadAndZip };
|
||||||
43
extensions/dicom-p10-downloader/src/index.js
Normal file
43
extensions/dicom-p10-downloader/src/index.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { getDicomWebClientFromConfig } from './utils';
|
||||||
|
import { getCommands } from './commandsModule';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globals
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sharedContext = {
|
||||||
|
dicomWebClient: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id: 'dicom-p10-downloader',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LIFECYCLE HOOKS
|
||||||
|
*/
|
||||||
|
|
||||||
|
preRegistration({ appConfig }) {
|
||||||
|
const dicomWebClient = getDicomWebClientFromConfig(appConfig);
|
||||||
|
if (dicomWebClient) {
|
||||||
|
sharedContext.dicomWebClient = dicomWebClient;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MODULE GETTERS
|
||||||
|
*/
|
||||||
|
|
||||||
|
getCommandsModule() {
|
||||||
|
return getCommands(sharedContext);
|
||||||
|
},
|
||||||
|
};
|
||||||
110
extensions/dicom-p10-downloader/src/utils.js
Normal file
110
extensions/dicom-p10-downloader/src/utils.js
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import { api } from 'dicomweb-client';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
|
const {
|
||||||
|
utils: { isDicomUid, resolveObjectPath, hierarchicalListUtils },
|
||||||
|
DICOMWeb,
|
||||||
|
} = OHIF;
|
||||||
|
|
||||||
|
function validDicomUid(subject) {
|
||||||
|
if (isDicomUid(subject)) {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveServerFromServersStore(store) {
|
||||||
|
const servers = resolveObjectPath(store, 'servers');
|
||||||
|
if (Array.isArray(servers) && servers.length > 0) {
|
||||||
|
return servers.find(server => resolveObjectPath(server, 'active') === true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDicomWebClientFromConfig(config) {
|
||||||
|
const servers = resolveObjectPath(config, 'servers.dicomWeb');
|
||||||
|
if (Array.isArray(servers) && servers.length > 0) {
|
||||||
|
const server = servers[0];
|
||||||
|
return new api.DICOMwebClient({
|
||||||
|
url: server.wadoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDicomWebClientFromContext(context, store) {
|
||||||
|
const activeServer = getActiveServerFromServersStore(store);
|
||||||
|
if (activeServer) {
|
||||||
|
return new api.DICOMwebClient({
|
||||||
|
url: activeServer.wadoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(activeServer),
|
||||||
|
});
|
||||||
|
} else if (context.dicomWebClient instanceof api.DICOMwebClient) {
|
||||||
|
return context.dicomWebClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSOPInstanceReference(viewports, index) {
|
||||||
|
if (index >= 0) {
|
||||||
|
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
|
||||||
|
resolveObjectPath(viewports, `viewportSpecificData.${index}`)
|
||||||
|
);
|
||||||
|
return Object.freeze(
|
||||||
|
hierarchicalListUtils.addToList(
|
||||||
|
[],
|
||||||
|
validDicomUid(StudyInstanceUID),
|
||||||
|
validDicomUid(SeriesInstanceUID),
|
||||||
|
validDicomUid(SOPInstanceUID)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSOPInstanceReferenceFromActiveViewport(viewports) {
|
||||||
|
return getSOPInstanceReference(
|
||||||
|
viewports,
|
||||||
|
resolveObjectPath(viewports, 'activeViewportIndex')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSOPInstanceReferencesFromViewports(viewports) {
|
||||||
|
const list = [];
|
||||||
|
const viewportSpecificData = resolveObjectPath(
|
||||||
|
viewports,
|
||||||
|
'viewportSpecificData'
|
||||||
|
);
|
||||||
|
Object.keys(viewportSpecificData).forEach(index => {
|
||||||
|
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
|
||||||
|
viewportSpecificData[index]
|
||||||
|
);
|
||||||
|
hierarchicalListUtils.addToList(
|
||||||
|
list,
|
||||||
|
validDicomUid(StudyInstanceUID),
|
||||||
|
validDicomUid(SeriesInstanceUID),
|
||||||
|
validDicomUid(SOPInstanceUID)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(promise, listOfUIDs) {
|
||||||
|
return Promise.resolve(promise)
|
||||||
|
.then(url => {
|
||||||
|
OHIF.log.info('Files successfully compressed:', url);
|
||||||
|
const StudyInstanceUID = hierarchicalListUtils.getItem(listOfUIDs, 0);
|
||||||
|
saveAs(url, `${StudyInstanceUID}.zip`);
|
||||||
|
return url;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
OHIF.log.error('Failed to create Zip file...', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
save,
|
||||||
|
validDicomUid,
|
||||||
|
getDicomWebClientFromConfig,
|
||||||
|
getDicomWebClientFromContext,
|
||||||
|
getSOPInstanceReferenceFromActiveViewport,
|
||||||
|
getSOPInstanceReferencesFromViewports,
|
||||||
|
};
|
||||||
66
platform/core/src/utils/Queue.js
Normal file
66
platform/core/src/utils/Queue.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
export default class Queue {
|
||||||
|
|
||||||
|
constructor(limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
this.size = 0;
|
||||||
|
this.awaiting = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new "proxy" function associated with the current execution queue
|
||||||
|
* instance. When the returned function is invoked, the queue limit is checked
|
||||||
|
* to make sure the limit of scheduled tasks is repected (throwing an
|
||||||
|
* exception when the limit has been reached and before calling the original
|
||||||
|
* function). The original function is only invoked after all the previously
|
||||||
|
* scheduled tasks have finished executing (their returned promises have
|
||||||
|
* resolved/rejected);
|
||||||
|
*
|
||||||
|
* @param {function} task The function whose execution will be associated
|
||||||
|
* with the current Queue instance;
|
||||||
|
* @returns {function} The "proxy" function bound to the current Queue
|
||||||
|
* instance;
|
||||||
|
*/
|
||||||
|
bind(task) {
|
||||||
|
return bind(this, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindSafe(task, onError) {
|
||||||
|
const boundTask = bind(this, task);
|
||||||
|
return async function safeTask(...args) {
|
||||||
|
try {
|
||||||
|
return await boundTask(...args);
|
||||||
|
} catch (e) {
|
||||||
|
onError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
function bind(queue, task) {
|
||||||
|
const cleaner = clean.bind(null, queue);
|
||||||
|
return async function boundTask(...args) {
|
||||||
|
if (queue.size >= queue.limit) {
|
||||||
|
throw new Error('Queue limit reached');
|
||||||
|
}
|
||||||
|
const promise = chain(queue.awaiting, task, args);
|
||||||
|
queue.awaiting = promise.then(cleaner, cleaner);
|
||||||
|
queue.size++;
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clean(queue) {
|
||||||
|
if (queue.size > 0 && --queue.size === 0) {
|
||||||
|
queue.awaiting = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chain(prev, task, args) {
|
||||||
|
await prev;
|
||||||
|
return task(...args);
|
||||||
|
}
|
||||||
66
platform/core/src/utils/Queue.test.js
Normal file
66
platform/core/src/utils/Queue.test.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import makeDeferred from './makeDeferred';
|
||||||
|
import Queue from './Queue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
function timeout(delay) {
|
||||||
|
const { resolve, promise } = makeDeferred();
|
||||||
|
setTimeout(() => void resolve(Date.now()), delay);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('Queue', () => {
|
||||||
|
it('should bind functions to the queue', async () => {
|
||||||
|
const queue = new Queue(2);
|
||||||
|
const mockedTimeout = jest.fn(timeout);
|
||||||
|
const timer = queue.bind(mockedTimeout);
|
||||||
|
const start = Date.now();
|
||||||
|
timer(120).then(now => {
|
||||||
|
const elapsed = now - start;
|
||||||
|
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||||
|
});
|
||||||
|
const end = await timer(120);
|
||||||
|
expect(end - start > 240).toBe(true);
|
||||||
|
expect(mockedTimeout).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
it('should prevent task execution when queue limit is reached', async () => {
|
||||||
|
const queue = new Queue(1);
|
||||||
|
const mockedTimeout = jest.fn(timeout);
|
||||||
|
const timer = queue.bind(mockedTimeout);
|
||||||
|
const start = Date.now();
|
||||||
|
const promise = timer(120).then(time => time - start);
|
||||||
|
try {
|
||||||
|
await timer(120);
|
||||||
|
} catch (e) {
|
||||||
|
expect(Date.now() - start < 120).toBe(true);
|
||||||
|
expect(e.message).toBe('Queue limit reached');
|
||||||
|
}
|
||||||
|
const elapsed = await promise;
|
||||||
|
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||||
|
expect(mockedTimeout).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
it('should safely bind tasks to the queue', async () => {
|
||||||
|
const queue = new Queue(1);
|
||||||
|
const mockedErrorHandler = jest.fn();
|
||||||
|
const mockedTimeout = jest.fn(timeout);
|
||||||
|
const timer = queue.bindSafe(mockedTimeout, mockedErrorHandler);
|
||||||
|
const start = Date.now();
|
||||||
|
const promise = timer(120).then(time => time - start);
|
||||||
|
await timer(120);
|
||||||
|
expect(Date.now() - start < 120).toBe(true);
|
||||||
|
expect(mockedErrorHandler).toBeCalledTimes(1);
|
||||||
|
expect(mockedErrorHandler).nthCalledWith(
|
||||||
|
1,
|
||||||
|
expect.objectContaining({ message: 'Queue limit reached' })
|
||||||
|
);
|
||||||
|
const elapsed = await promise;
|
||||||
|
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||||
|
expect(mockedTimeout).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
193
platform/core/src/utils/hierarchicalListUtils.js
Normal file
193
platform/core/src/utils/hierarchicalListUtils.js
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SEPARATOR = '/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add values to a list hierarchically.
|
||||||
|
* @ For example:
|
||||||
|
* addToList([], 'a', 'b', 'c');
|
||||||
|
* will add the following hierarchy to the list:
|
||||||
|
* a > b > c
|
||||||
|
* resulting in the following array:
|
||||||
|
* [['a', [['b', ['c']]]]]
|
||||||
|
* @param {Array} list The target list;
|
||||||
|
* @param {...string} values The values to be hierarchically added to the list;
|
||||||
|
* @returns {Array} Returns the provided list possibly updated with the given
|
||||||
|
* values or null when a bad list (not an actual array) is provided
|
||||||
|
*/
|
||||||
|
function addToList(list, ...values) {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
if (values.length > 0) {
|
||||||
|
addValuesToList(list, values);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates through the provided hierarchical list executing the callback
|
||||||
|
* once for each leaf-node of the tree. The ancestors of the leaf-node being
|
||||||
|
* visited are passed to the callback function along with the leaf-node in
|
||||||
|
* the exact same order they appear on the tree (from root to leaf);
|
||||||
|
* @ For example, if the hierachy `a > b > c` appears on the tree ("a" being
|
||||||
|
* the root and "c" being the leaf) the callback function will be called as:
|
||||||
|
* callback('a', 'b', 'c');
|
||||||
|
* @param {Array} list The hierarchical list to be iterated
|
||||||
|
* @param {function} callback The callback which will be exected once for
|
||||||
|
* each leaf-node of the hierarchical list;
|
||||||
|
* @returns {Array} Returns the provided list or null for bad arguments;
|
||||||
|
*/
|
||||||
|
function forEach(list, callback) {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
forEachValue(list, callback);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an item from the given hierarchical list based on an index (number)
|
||||||
|
* or a path (string).
|
||||||
|
* @ For example:
|
||||||
|
* getItem(list, '1/0/4')
|
||||||
|
* will retrieve the fourth grandchild, from the first child of the second
|
||||||
|
* element of the list;
|
||||||
|
* @param {Array} list The source list;
|
||||||
|
* @param {string|number} indexOrPath The index of the element inside list
|
||||||
|
* (number) or the path to reach the desired element (string). The slash "/"
|
||||||
|
* character is cosidered the path separator;
|
||||||
|
*/
|
||||||
|
function getItem(list, indexOrPath) {
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
let subpath = null;
|
||||||
|
let index = typeof indexOrPath === 'number' ? indexOrPath : -1;
|
||||||
|
if (typeof indexOrPath === 'string') {
|
||||||
|
const separator = indexOrPath.indexOf(SEPARATOR);
|
||||||
|
if (separator > 0) {
|
||||||
|
index = parseInt(indexOrPath.slice(0, separator), 10);
|
||||||
|
if (separator + 1 < indexOrPath.length) {
|
||||||
|
subpath = indexOrPath.slice(separator + 1, indexOrPath.length);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
index = parseInt(indexOrPath, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= 0 && index < list.length) {
|
||||||
|
const item = list[index];
|
||||||
|
if (isSublist(item)) {
|
||||||
|
if (subpath !== null) {
|
||||||
|
return getItem(item[1], subpath);
|
||||||
|
}
|
||||||
|
return item[0];
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty-print the provided hierarchical list;
|
||||||
|
* @param {Array} list The source list;
|
||||||
|
* @returns {string} The textual representation of the hierarchical list;
|
||||||
|
*/
|
||||||
|
function print(list) {
|
||||||
|
let text = '';
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
let prev = [];
|
||||||
|
forEachValue(list, function(...args) {
|
||||||
|
let prevLen = prev.length;
|
||||||
|
for (let i = 0, l = args.length; i < l; ++i) {
|
||||||
|
if (i < prevLen && args[i] === prev[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
text += ' '.repeat(i) + args[i] + '\n';
|
||||||
|
}
|
||||||
|
prev = args;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
function forEachValue(list, callback) {
|
||||||
|
for (let i = 0, l = list.length; i < l; ++i) {
|
||||||
|
let item = list[i];
|
||||||
|
if (isSublist(item)) {
|
||||||
|
if (item[1].length > 0) {
|
||||||
|
forEachValue(item[1], callback.bind(null, item[0]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
item = item[0];
|
||||||
|
}
|
||||||
|
callback(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addValuesToList(list, values) {
|
||||||
|
let value = values.shift();
|
||||||
|
let index = add(list, value);
|
||||||
|
if (index >= 0) {
|
||||||
|
if (values.length > 0) {
|
||||||
|
let sublist = list[index];
|
||||||
|
if (!isSublist(sublist)) {
|
||||||
|
sublist = toSublist(value);
|
||||||
|
list[index] = sublist;
|
||||||
|
}
|
||||||
|
return addValuesToList(sublist[1], values);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(list, value) {
|
||||||
|
let index = find(list, value);
|
||||||
|
if (index === -2) {
|
||||||
|
index = list.push(value) - 1;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function find(list, value) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
for (let i = 0, l = list.length; i < l; ++i) {
|
||||||
|
let item = list[i];
|
||||||
|
if (item === value || (isSublist(item) && item[0] === value)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSublist(subject) {
|
||||||
|
return (
|
||||||
|
Array.isArray(subject) &&
|
||||||
|
subject.length === 2 &&
|
||||||
|
typeof subject[0] === 'string' &&
|
||||||
|
Array.isArray(subject[1])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSublist(value) {
|
||||||
|
return [value + '', []];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { addToList, getItem, forEach, print };
|
||||||
96
platform/core/src/utils/hierarchicalListUtils.test.js
Normal file
96
platform/core/src/utils/hierarchicalListUtils.test.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { addToList, forEach, getItem, print } from './hierarchicalListUtils';
|
||||||
|
|
||||||
|
describe('hierarchicalListUtils', function() {
|
||||||
|
let sharedList;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
sharedList = [
|
||||||
|
['1.2.3.1', ['1.2.3.1.1', '1.2.3.1.2']],
|
||||||
|
'1.2.3.2',
|
||||||
|
['1.2.3.3', ['1.2.3.3.1', ['1.2.3.3.2', ['1.2.3.3.2.1', '1.2.3.3.2.2']]]],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getItem', function() {
|
||||||
|
it('should retrieve elements from a list by index', function() {
|
||||||
|
expect(getItem(sharedList, 0)).toBe('1.2.3.1');
|
||||||
|
expect(getItem(sharedList, 1)).toBe('1.2.3.2');
|
||||||
|
expect(getItem(sharedList, 2)).toBe('1.2.3.3');
|
||||||
|
expect(getItem(sharedList, 3)).toBeUndefined();
|
||||||
|
});
|
||||||
|
it('should retrieve elements from a list by path', function() {
|
||||||
|
expect(getItem(sharedList, '0')).toBe('1.2.3.1');
|
||||||
|
expect(getItem(sharedList, '0/0')).toBe('1.2.3.1.1');
|
||||||
|
expect(getItem(sharedList, '0/1')).toBe('1.2.3.1.2');
|
||||||
|
expect(getItem(sharedList, '0/2')).toBeUndefined();
|
||||||
|
expect(getItem(sharedList, '1')).toBe('1.2.3.2');
|
||||||
|
expect(getItem(sharedList, '2')).toBe('1.2.3.3');
|
||||||
|
expect(getItem(sharedList, '2/0')).toBe('1.2.3.3.1');
|
||||||
|
expect(getItem(sharedList, '2/1')).toBe('1.2.3.3.2');
|
||||||
|
expect(getItem(sharedList, '2/2')).toBeUndefined();
|
||||||
|
expect(getItem(sharedList, '2/1/0')).toBe('1.2.3.3.2.1');
|
||||||
|
expect(getItem(sharedList, '2/1/1')).toBe('1.2.3.3.2.2');
|
||||||
|
expect(getItem(sharedList, '2/1/2')).toBeUndefined();
|
||||||
|
expect(getItem(sharedList, '3')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addToList', function() {
|
||||||
|
it('should support adding elements to a list hierarchically', function() {
|
||||||
|
const list = [];
|
||||||
|
addToList(list, '1.2.3.1', '1.2.3.1.1');
|
||||||
|
addToList(list, '1.2.3.1', '1.2.3.1.2');
|
||||||
|
addToList(list, '1.2.3.2');
|
||||||
|
addToList(list, '1.2.3.3', '1.2.3.3.1');
|
||||||
|
addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1');
|
||||||
|
addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2');
|
||||||
|
expect(list).toStrictEqual(sharedList);
|
||||||
|
});
|
||||||
|
it('should change leaf nodes into non-leaf nodes', function() {
|
||||||
|
const listw = [];
|
||||||
|
const listx = [['x.1', ['x.1.1', 'x.1.2']], 'x.2'];
|
||||||
|
const listy = [
|
||||||
|
['x.1', [['x.1.1', ['x.1.1.1']], 'x.1.2']],
|
||||||
|
['x.2', ['x.2.1']],
|
||||||
|
];
|
||||||
|
addToList(listw, 'x.1');
|
||||||
|
addToList(listw, 'x.1', 'x.1.1');
|
||||||
|
addToList(listw, 'x.1', 'x.1.2');
|
||||||
|
addToList(listw, 'x.2');
|
||||||
|
expect(listw).toStrictEqual(listx);
|
||||||
|
addToList(listw, 'x.2', 'x.2.1');
|
||||||
|
addToList(listw, 'x.1', 'x.1.1', 'x.1.1.1');
|
||||||
|
expect(listw).toStrictEqual(listy);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forEach', function() {
|
||||||
|
it('should iterate through all leaf nodes of the tree', function() {
|
||||||
|
const fn = jest.fn();
|
||||||
|
forEach(sharedList, fn);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(6);
|
||||||
|
expect(fn).nthCalledWith(1, '1.2.3.1', '1.2.3.1.1');
|
||||||
|
expect(fn).nthCalledWith(2, '1.2.3.1', '1.2.3.1.2');
|
||||||
|
expect(fn).nthCalledWith(3, '1.2.3.2');
|
||||||
|
expect(fn).nthCalledWith(4, '1.2.3.3', '1.2.3.3.1');
|
||||||
|
expect(fn).nthCalledWith(5, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1');
|
||||||
|
expect(fn).nthCalledWith(6, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('print', function() {
|
||||||
|
it('should pretty-print the hierarchical list', function() {
|
||||||
|
expect(print(sharedList)).toBe(
|
||||||
|
'1.2.3.1\n' +
|
||||||
|
' 1.2.3.1.1\n' +
|
||||||
|
' 1.2.3.1.2\n' +
|
||||||
|
'1.2.3.2\n' +
|
||||||
|
'1.2.3.3\n' +
|
||||||
|
' 1.2.3.3.1\n' +
|
||||||
|
' 1.2.3.3.2\n' +
|
||||||
|
' 1.2.3.3.2.1\n' +
|
||||||
|
' 1.2.3.3.2.2\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -10,8 +10,14 @@ import DicomLoaderService from './dicomLoaderService.js';
|
|||||||
import b64toBlob from './b64toBlob.js';
|
import b64toBlob from './b64toBlob.js';
|
||||||
import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js';
|
import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js';
|
||||||
import * as urlUtil from './urlUtil';
|
import * as urlUtil from './urlUtil';
|
||||||
|
import makeDeferred from './makeDeferred';
|
||||||
import makeCancelable from './makeCancelable';
|
import makeCancelable from './makeCancelable';
|
||||||
import hotkeys from './hotkeys';
|
import hotkeys from './hotkeys';
|
||||||
|
import Queue from './Queue';
|
||||||
|
import isDicomUid from './isDicomUid';
|
||||||
|
import resolveObjectPath from './resolveObjectPath';
|
||||||
|
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
||||||
|
import * as progressTrackingUtils from './progressTrackingUtils';
|
||||||
|
|
||||||
const utils = {
|
const utils = {
|
||||||
guid,
|
guid,
|
||||||
@ -26,8 +32,14 @@ const utils = {
|
|||||||
DicomLoaderService,
|
DicomLoaderService,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
loadAndCacheDerivedDisplaySets,
|
loadAndCacheDerivedDisplaySets,
|
||||||
|
makeDeferred,
|
||||||
makeCancelable,
|
makeCancelable,
|
||||||
hotkeys,
|
hotkeys,
|
||||||
|
Queue,
|
||||||
|
isDicomUid,
|
||||||
|
resolveObjectPath,
|
||||||
|
hierarchicalListUtils,
|
||||||
|
progressTrackingUtils,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -43,8 +55,14 @@ export {
|
|||||||
DicomLoaderService,
|
DicomLoaderService,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
loadAndCacheDerivedDisplaySets,
|
loadAndCacheDerivedDisplaySets,
|
||||||
|
makeDeferred,
|
||||||
makeCancelable,
|
makeCancelable,
|
||||||
hotkeys,
|
hotkeys,
|
||||||
|
Queue,
|
||||||
|
isDicomUid,
|
||||||
|
resolveObjectPath,
|
||||||
|
hierarchicalListUtils,
|
||||||
|
progressTrackingUtils,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default utils;
|
export default utils;
|
||||||
|
|||||||
@ -15,8 +15,14 @@ describe('Top level exports', () => {
|
|||||||
'loadAndCacheDerivedDisplaySets',
|
'loadAndCacheDerivedDisplaySets',
|
||||||
'DicomLoaderService',
|
'DicomLoaderService',
|
||||||
'urlUtil',
|
'urlUtil',
|
||||||
|
'makeDeferred',
|
||||||
'makeCancelable',
|
'makeCancelable',
|
||||||
'hotkeys',
|
'hotkeys',
|
||||||
|
'Queue',
|
||||||
|
'isDicomUid',
|
||||||
|
'resolveObjectPath',
|
||||||
|
'hierarchicalListUtils',
|
||||||
|
'progressTrackingUtils',
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(utils.default).sort();
|
const exports = Object.keys(utils.default).sort();
|
||||||
|
|||||||
4
platform/core/src/utils/isDicomUid.js
Normal file
4
platform/core/src/utils/isDicomUid.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default function isDicomUid(subject) {
|
||||||
|
const regex = /^\d+(?:\.\d+)*$/;
|
||||||
|
return typeof subject === 'string' && regex.test(subject.trim());
|
||||||
|
}
|
||||||
16
platform/core/src/utils/isDicomUid.test.js
Normal file
16
platform/core/src/utils/isDicomUid.test.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import isDicomUid from './isDicomUid';
|
||||||
|
|
||||||
|
describe('isDicomUid', function() {
|
||||||
|
it('should return true for valid DICOM UIDs', function() {
|
||||||
|
expect(isDicomUid('1')).toBe(true);
|
||||||
|
expect(isDicomUid('1.2')).toBe(true);
|
||||||
|
expect(isDicomUid('1.2.3')).toBe(true);
|
||||||
|
expect(isDicomUid('1.2.3.4')).toBe(true);
|
||||||
|
});
|
||||||
|
it('should return false for invalid DICOM UIDs', function() {
|
||||||
|
expect(isDicomUid('x')).toBe(false);
|
||||||
|
expect(isDicomUid('1.')).toBe(false);
|
||||||
|
expect(isDicomUid('1. 2')).toBe(false);
|
||||||
|
expect(isDicomUid('1.2.n.4')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
7
platform/core/src/utils/makeDeferred.js
Normal file
7
platform/core/src/utils/makeDeferred.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function makeDeferred() {
|
||||||
|
let reject, resolve, promise = new Promise(function (res, rej) {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return Object.freeze({ promise, resolve, reject });
|
||||||
|
}
|
||||||
14
platform/core/src/utils/makeDeferred.test.js
Normal file
14
platform/core/src/utils/makeDeferred.test.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import makeDeferred from './makeDeferred';
|
||||||
|
|
||||||
|
describe('makeDeferred', () => {
|
||||||
|
it('should provide a promise to be resolved externally', () => {
|
||||||
|
const deferred = makeDeferred();
|
||||||
|
setTimeout(() => void deferred.resolve('Yay!'));
|
||||||
|
return deferred.promise.then(result => void expect(result).toBe('Yay!'));
|
||||||
|
});
|
||||||
|
it('should provide a promise to be rejected externally', () => {
|
||||||
|
const deferred = makeDeferred();
|
||||||
|
setTimeout(() => void deferred.reject('Oops...'));
|
||||||
|
return deferred.promise.catch(error => void expect(error).toBe('Oops...'));
|
||||||
|
});
|
||||||
|
});
|
||||||
336
platform/core/src/utils/progressTrackingUtils.js
Normal file
336
platform/core/src/utils/progressTrackingUtils.js
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import makeDeferred from './makeDeferred';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TYPE = Symbol('Type');
|
||||||
|
const TASK = Symbol('Task');
|
||||||
|
const LIST = Symbol('List');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of a task list
|
||||||
|
* @returns {Object} A task list object
|
||||||
|
*/
|
||||||
|
function createList() {
|
||||||
|
return objectWithType(LIST, {
|
||||||
|
head: null,
|
||||||
|
named: Object.create(null),
|
||||||
|
observers: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given argument is a List instance
|
||||||
|
* @param {any} subject The value to be tested
|
||||||
|
* @returns {boolean} true if a valid List instance is given, false otherwise
|
||||||
|
*/
|
||||||
|
function isList(subject) {
|
||||||
|
return isOfType(LIST, subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of a task
|
||||||
|
* @param {Object} list The List instance related to this task
|
||||||
|
* @param {Object} next The next Task instance to link to
|
||||||
|
* @returns {Object} A task object
|
||||||
|
*/
|
||||||
|
function createTask(list, next) {
|
||||||
|
return objectWithType(TASK, {
|
||||||
|
list: isList(list) ? list : null,
|
||||||
|
next: isTask(next) ? next : null,
|
||||||
|
failed: false,
|
||||||
|
awaiting: null,
|
||||||
|
progress: 0.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given argument is a Task instance
|
||||||
|
* @param {any} subject The value to be tested
|
||||||
|
* @returns {boolean} true if a valid Task instance is given, false otherwise
|
||||||
|
*/
|
||||||
|
function isTask(subject) {
|
||||||
|
return isOfType(TASK, subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a new Task to the given List instance and notifies the list observers
|
||||||
|
* @param {Object} list A List instance
|
||||||
|
* @returns {Object} The new Task instance appended to the List or null if the
|
||||||
|
* given List instanc is not valid
|
||||||
|
*/
|
||||||
|
function increaseList(list) {
|
||||||
|
if (isList(list)) {
|
||||||
|
const task = createTask(list, list.head);
|
||||||
|
list.head = task;
|
||||||
|
notify(list, getOverallProgress(list));
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the internal progress value of the given Task instance and notifies
|
||||||
|
* the observers of the associated list.
|
||||||
|
* @param {Object} task The Task instance to be updated
|
||||||
|
* @param {number} value A number between 0 (inclusive) and 1 (exclusive)
|
||||||
|
* indicating the progress of the task;
|
||||||
|
* @returns {void} Nothing is returned
|
||||||
|
*/
|
||||||
|
function update(task, value) {
|
||||||
|
if (isTask(task) && isValidProgress(value) && value < 1.0) {
|
||||||
|
if (task.progress !== value) {
|
||||||
|
task.progress = value;
|
||||||
|
if (isList(task.list)) {
|
||||||
|
notify(task.list, getOverallProgress(task.list));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Task instance as finished (progress = 1.0), freezes it in order to
|
||||||
|
* prevent further modifications and notifies the observers of the associated
|
||||||
|
* list.
|
||||||
|
* @param {Object} task The Task instance to be finalized
|
||||||
|
* @returns {void} Nothing is returned
|
||||||
|
*/
|
||||||
|
function finish(task) {
|
||||||
|
if (isTask(task)) {
|
||||||
|
task.progress = 1.0;
|
||||||
|
task.awaiting = null;
|
||||||
|
Object.freeze(task);
|
||||||
|
if (isList(task.list)) {
|
||||||
|
notify(task.list, getOverallProgress(task.list));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a summarized snapshot of the current status of the given task List
|
||||||
|
* @param {Object} list The List instance to be scanned
|
||||||
|
* @returns {Object} An obeject representing the summarized status of the list
|
||||||
|
*/
|
||||||
|
function getOverallProgress(list) {
|
||||||
|
const status = createStatus();
|
||||||
|
if (isList(list)) {
|
||||||
|
let task = list.head;
|
||||||
|
while (isTask(task)) {
|
||||||
|
status.total++;
|
||||||
|
if (isValidProgress(task.progress)) {
|
||||||
|
status.partial += task.progress;
|
||||||
|
if (task.progress === 1.0 && task.failed) status.failures++;
|
||||||
|
}
|
||||||
|
task = task.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (status.total > 0) {
|
||||||
|
status.progress = status.partial / status.total;
|
||||||
|
}
|
||||||
|
return Object.freeze(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a Task instance to the given list that waits on a given "thenable". When
|
||||||
|
* the thenable resolves the "finish" method is called on the newly created
|
||||||
|
* instance thus notifying the observers of the list.
|
||||||
|
* @param {Object} list The List instance to which the new task will be added
|
||||||
|
* @param {Object|Promise} thenable The thenable to be waited on
|
||||||
|
* @returns {Object} A reference to the newly created Task;
|
||||||
|
*/
|
||||||
|
function waitOn(list, thenable) {
|
||||||
|
const task = increaseList(list);
|
||||||
|
if (isTask(task)) {
|
||||||
|
task.awaiting = Promise.resolve(thenable).then(
|
||||||
|
function() {
|
||||||
|
finish(task);
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
task.failed = true;
|
||||||
|
finish(task);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a Task instance to the given list using a deferred (a Promise that can
|
||||||
|
* be externally resolved) notifying the observers of the list.
|
||||||
|
* @param {Object} list The List instance to which the new task will be added
|
||||||
|
* @returns {Object} An object with references to the created deferred and task
|
||||||
|
*/
|
||||||
|
function addDeferred(list) {
|
||||||
|
const deferred = makeDeferred();
|
||||||
|
const task = waitOn(list, deferred.promise);
|
||||||
|
return Object.freeze({
|
||||||
|
deferred,
|
||||||
|
task,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns a name to a specific task of the list
|
||||||
|
* @param {Object} list The List instance whose task will be named
|
||||||
|
* @param {Object} task The specified Task instance
|
||||||
|
* @param {string} name The name of the task
|
||||||
|
* @returns {boolean} Returns true on success, false otherwise
|
||||||
|
*/
|
||||||
|
function setTaskName(list, task, name) {
|
||||||
|
if (
|
||||||
|
contains(list, task) &&
|
||||||
|
list.named !== null &&
|
||||||
|
typeof list.named === 'object' &&
|
||||||
|
typeof name === 'string'
|
||||||
|
) {
|
||||||
|
list.named[name] = task;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a task by name
|
||||||
|
* @param {Object} list The List instance whose task will be retrieved
|
||||||
|
* @param {string} name The name of the task to be retrieved
|
||||||
|
* @returns {Object} The Task instance or null if not found
|
||||||
|
*/
|
||||||
|
function getTaskByName(list, name) {
|
||||||
|
if (
|
||||||
|
isList(list) &&
|
||||||
|
list.named !== null &&
|
||||||
|
typeof list.named === 'object' &&
|
||||||
|
typeof name === 'string'
|
||||||
|
) {
|
||||||
|
const task = list.named[name];
|
||||||
|
if (isTask(task)) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an observer (callback function) to a given List instance
|
||||||
|
* @param {Object} list The List instance to which the observer will be appended
|
||||||
|
* @param {Function} observer The observer (function) that will be executed
|
||||||
|
* every time a change happens within the list
|
||||||
|
* @returns {boolean} Returns true on success and false otherewise
|
||||||
|
*/
|
||||||
|
function addObserver(list, observer) {
|
||||||
|
if (
|
||||||
|
isList(list) &&
|
||||||
|
Array.isArray(list.observers) &&
|
||||||
|
typeof observer === 'function'
|
||||||
|
) {
|
||||||
|
list.observers.push(observer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an observer (callback function) from a given List instance
|
||||||
|
* @param {Object} list The instance List from which the observer will removed
|
||||||
|
* @param {Function} observer The observer function to be removed
|
||||||
|
* @returns {boolean} Returns true on success and false otherewise
|
||||||
|
*/
|
||||||
|
function removeObserver(list, observer) {
|
||||||
|
if (
|
||||||
|
isList(list) &&
|
||||||
|
Array.isArray(list.observers) &&
|
||||||
|
list.observers.length > 0
|
||||||
|
) {
|
||||||
|
const index = list.observers.indexOf(observer);
|
||||||
|
if (index >= 0) {
|
||||||
|
list.observers.splice(index, 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utils
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createStatus() {
|
||||||
|
return Object.seal({
|
||||||
|
total: 0,
|
||||||
|
partial: 0.0,
|
||||||
|
progress: 0.0,
|
||||||
|
failures: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectWithType(type, object) {
|
||||||
|
return Object.seal(Object.defineProperty(object, TYPE, { value: type }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOfType(type, subject) {
|
||||||
|
return (
|
||||||
|
subject !== null && typeof subject === 'object' && subject[TYPE] === type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidProgress(value) {
|
||||||
|
return typeof value === 'number' && value >= 0.0 && value <= 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contains(list, task) {
|
||||||
|
if (isList(list) && isTask(task)) {
|
||||||
|
let item = list.head;
|
||||||
|
while (isTask(item)) {
|
||||||
|
if (item === task) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
item = item.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(list, data) {
|
||||||
|
if (
|
||||||
|
isList(list) &&
|
||||||
|
Array.isArray(list.observers) &&
|
||||||
|
list.observers.length > 0
|
||||||
|
) {
|
||||||
|
list.observers.slice().forEach(function(observer) {
|
||||||
|
if (typeof observer === 'function') {
|
||||||
|
try {
|
||||||
|
observer(data, list);
|
||||||
|
} catch (e) {
|
||||||
|
/* Oops! */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
createList,
|
||||||
|
isList,
|
||||||
|
createTask,
|
||||||
|
isTask,
|
||||||
|
increaseList,
|
||||||
|
update,
|
||||||
|
finish,
|
||||||
|
getOverallProgress,
|
||||||
|
waitOn,
|
||||||
|
addDeferred,
|
||||||
|
setTaskName,
|
||||||
|
getTaskByName,
|
||||||
|
addObserver,
|
||||||
|
removeObserver,
|
||||||
|
};
|
||||||
175
platform/core/src/utils/progressTrackingUtils.test.js
Normal file
175
platform/core/src/utils/progressTrackingUtils.test.js
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import * as utils from './progressTrackingUtils';
|
||||||
|
|
||||||
|
describe('progressTrackingUtils', () => {
|
||||||
|
describe('Creation of lists of tasks to be tracked', () => {
|
||||||
|
it('should support creation of task lists', () => {
|
||||||
|
expect(utils.createList()).toBeInstanceOf(Object);
|
||||||
|
});
|
||||||
|
it('should support validation of task lists', () => {
|
||||||
|
const list = utils.createList();
|
||||||
|
expect(utils.isList(list)).toBe(true);
|
||||||
|
expect(utils.isList(JSON.parse(JSON.stringify(list)))).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Usage of lists of tasks to be tracked', () => {
|
||||||
|
let context;
|
||||||
|
|
||||||
|
// Mock for download
|
||||||
|
function fakeRequest(callback) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let progress = 0.0;
|
||||||
|
setTimeout(function step() {
|
||||||
|
if (progress < 1.0) {
|
||||||
|
progress += 1 / 4;
|
||||||
|
callback(progress);
|
||||||
|
setTimeout(step);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const list = utils.createList();
|
||||||
|
const observer = jest.fn();
|
||||||
|
utils.addObserver(list, observer);
|
||||||
|
context = { list, observer };
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call observer twice for each task', () => {
|
||||||
|
const { list, observer } = context;
|
||||||
|
const promises = [
|
||||||
|
Promise.resolve('A'),
|
||||||
|
Promise.resolve('B'),
|
||||||
|
Promise.resolve('C'),
|
||||||
|
];
|
||||||
|
promises.forEach(promise => void utils.waitOn(list, promise));
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
expect(observer).toBeCalledTimes(6);
|
||||||
|
[
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0,
|
||||||
|
progress: 0,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0,
|
||||||
|
progress: 0,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0,
|
||||||
|
progress: 0,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 1.0,
|
||||||
|
progress: 1 / 3,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 2.0,
|
||||||
|
progress: 2 / 3,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 3.0,
|
||||||
|
progress: 1.0,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
|
].forEach((item, i) => {
|
||||||
|
const result = expect.objectContaining(item);
|
||||||
|
expect(observer).nthCalledWith(i + 1, result, list);
|
||||||
|
});
|
||||||
|
expect(utils.getOverallProgress(list)).toStrictEqual({
|
||||||
|
failures: 0,
|
||||||
|
partial: 3.0,
|
||||||
|
progress: 1.0,
|
||||||
|
total: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support tasks with internal progress updates', () => {
|
||||||
|
const { list, observer } = context;
|
||||||
|
const download = utils.addDeferred(list);
|
||||||
|
const processing = download.deferred.promise.then(result => result);
|
||||||
|
const update = jest.fn(p => void utils.update(download.task, p));
|
||||||
|
download.deferred.resolve(fakeRequest(update));
|
||||||
|
utils.waitOn(list, processing);
|
||||||
|
return processing.then(() => {
|
||||||
|
expect(update).toBeCalledTimes(4);
|
||||||
|
[0.25, 0.5, 0.75, 1.0].forEach(
|
||||||
|
(value, i) => void expect(update).nthCalledWith(i + 1, value)
|
||||||
|
);
|
||||||
|
expect(observer).toBeCalledTimes(7);
|
||||||
|
[
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0,
|
||||||
|
progress: 0,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0,
|
||||||
|
progress: 0,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0.25,
|
||||||
|
progress: 0.125,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0.5,
|
||||||
|
progress: 0.25,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 0.75,
|
||||||
|
progress: 0.375,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 1.0,
|
||||||
|
progress: 0.5,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failures: 0,
|
||||||
|
partial: 2.0,
|
||||||
|
progress: 1.0,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
].forEach((item, i) => {
|
||||||
|
const result = expect.objectContaining(item);
|
||||||
|
expect(observer).nthCalledWith(i + 1, result, list);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Naming of specific tasks', () => {
|
||||||
|
it('should support naming specific tasks', () => {
|
||||||
|
const list = utils.createList();
|
||||||
|
const tasks = [utils.increaseList(list), utils.increaseList(list)];
|
||||||
|
expect(utils.setTaskName(list, tasks[0], 'firstTask')).toBe(true);
|
||||||
|
expect(utils.setTaskName(list, tasks[1], 'secondTask')).toBe(true);
|
||||||
|
expect(utils.getTaskByName(list, 'secondTask')).toBe(tasks[1]);
|
||||||
|
expect(utils.getTaskByName(list, 'firstTask')).toBe(tasks[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
platform/core/src/utils/resolveObjectPath.js
Normal file
17
platform/core/src/utils/resolveObjectPath.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export default function resolveObjectPath(root, path, defaultValue) {
|
||||||
|
if (root !== null && typeof root === 'object' && typeof path === 'string') {
|
||||||
|
let value,
|
||||||
|
separator = path.indexOf('.');
|
||||||
|
if (separator >= 0) {
|
||||||
|
return resolveObjectPath(
|
||||||
|
root[path.slice(0, separator)],
|
||||||
|
path.slice(separator + 1, path.length),
|
||||||
|
defaultValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
value = root[path];
|
||||||
|
return value === undefined && defaultValue !== undefined
|
||||||
|
? defaultValue
|
||||||
|
: value;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
platform/core/src/utils/resolveObjectPath.test.js
Normal file
35
platform/core/src/utils/resolveObjectPath.test.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import resolveObjectPath from './resolveObjectPath';
|
||||||
|
|
||||||
|
describe('resolveObjectPath', function() {
|
||||||
|
let config;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
config = {
|
||||||
|
active: {
|
||||||
|
user: {
|
||||||
|
name: {
|
||||||
|
first: 'John',
|
||||||
|
last: 'Doe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
servers: [
|
||||||
|
{
|
||||||
|
ipv4: '10.0.0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should safely return deeply nested values from an object', function() {
|
||||||
|
expect(resolveObjectPath(config, 'active.user.name.first')).toBe('John');
|
||||||
|
expect(resolveObjectPath(config, 'active.user.name.last')).toBe('Doe');
|
||||||
|
expect(resolveObjectPath(config, 'active.servers.0.ipv4')).toBe('10.0.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should silently return undefined when intermediate values are not valid objects', function() {
|
||||||
|
expect(resolveObjectPath(config, 'active.usr.name.first')).toBeUndefined();
|
||||||
|
expect(resolveObjectPath(config, 'active.name.last')).toBeUndefined();
|
||||||
|
expect(resolveObjectPath(config, 'active.servers.7.ipv4')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -56,6 +56,7 @@
|
|||||||
"@ohif/extension-dicom-rt": "^0.0.2",
|
"@ohif/extension-dicom-rt": "^0.0.2",
|
||||||
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
||||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
|
"@ohif/extension-dicom-p10-downloader": "^0.0.1",
|
||||||
"@ohif/extension-vtk": "^1.3.11",
|
"@ohif/extension-vtk": "^1.3.11",
|
||||||
"@ohif/i18n": "^0.52.8",
|
"@ohif/i18n": "^0.52.8",
|
||||||
"@ohif/ui": "^1.4.3",
|
"@ohif/ui": "^1.4.3",
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
|||||||
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
|
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
|
||||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||||
|
import OHIFDicomP10DownloaderExtension from '@ohif/extension-dicom-p10-downloader';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Default Settings
|
* Default Settings
|
||||||
@ -48,6 +49,7 @@ const appProps = {
|
|||||||
OHIFDicomPDFExtension,
|
OHIFDicomPDFExtension,
|
||||||
OHIFDicomSegmentationExtension,
|
OHIFDicomSegmentationExtension,
|
||||||
OHIFDicomRtExtension,
|
OHIFDicomRtExtension,
|
||||||
|
OHIFDicomP10DownloaderExtension,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user