Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0cfc45f83 | ||
|
|
450c80b9d5 | ||
|
|
d3eefb50f5 | ||
|
|
29d4955980 | ||
|
|
2412c08595 | ||
|
|
d769e5c296 | ||
|
|
d21af3f133 | ||
|
|
70672ed663 | ||
|
|
858484f250 |
No files matched your search
@@ -126,12 +126,39 @@ globally defined styles/scripts.
|
|||||||
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
|
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
|
||||||
> react-dom and react conflicts. What can we do?
|
> react-dom and react conflicts. What can we do?
|
||||||
|
|
||||||
|
If you are installing OHIF viewer inside another react app, you may use `installViewer` as follows:
|
||||||
|
```
|
||||||
|
import { installViewer } from '@ohif/viewer'
|
||||||
|
|
||||||
|
const ohifViewerConfig = window.config // or set it here
|
||||||
|
const containerId = 'ohif'
|
||||||
|
const componentRenderedOrUpdatedCallback = function() {
|
||||||
|
console.log('OHIF Viewer rendered/updated');
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
installViewer(
|
||||||
|
ohifViewerConfig,
|
||||||
|
containerId,
|
||||||
|
componentRenderedOrUpdatedCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
...
|
||||||
|
//you can render in any element you wish
|
||||||
|
<AnyTag id={containerId}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
`installViewer` is a convenience method that pulls in some dependencies that may
|
`installViewer` is a convenience method that pulls in some dependencies that may
|
||||||
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
|
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
|
||||||
which is a react component that takes the `configuration` outlined above as
|
which is a react component that takes the `configuration` outlined above as
|
||||||
props. You can use it as a reusable component, and to avoid `react` version
|
props. You can use it as a reusable component, and to avoid `react` version
|
||||||
conflict issues.
|
conflict issues.
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
LINKS
|
LINKS
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.1...@ohif/extension-cornerstone@2.5.2) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
|
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "2.5.1",
|
"version": "2.5.2",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { getEnabledElement } from './state';
|
|||||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
const scroll = cornerstoneTools.import('util/scroll');
|
const scroll = cornerstoneTools.import('util/scroll');
|
||||||
|
|
||||||
|
const { studyMetadataManager } = OHIF.utils;
|
||||||
|
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager }) => {
|
const commandsModule = ({ servicesManager }) => {
|
||||||
const actions = {
|
const actions = {
|
||||||
rotateViewport: ({ viewports, rotation }) => {
|
rotateViewport: ({ viewports, rotation }) => {
|
||||||
@@ -255,9 +258,35 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
cornerstone.setViewport(enabledElement, viewport);
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
jumpToImage: ({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex
|
||||||
|
}) => {
|
||||||
|
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
|
||||||
|
const displaySet = study.findDisplaySet(ds => {
|
||||||
|
return ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||||
|
displaySet.frameIndex = frameIndex;
|
||||||
|
|
||||||
|
window.store.dispatch(setViewportSpecificData(activeViewportIndex, displaySet));
|
||||||
|
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
jumpToImage: {
|
||||||
|
commandFn: actions.jumpToImage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
getNearbyToolData: {
|
getNearbyToolData: {
|
||||||
commandFn: actions.getNearbyToolData,
|
commandFn: actions.getNearbyToolData,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## 0.0.2 (2020-04-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# @ohif/extension-dicom-p10-downloader
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-dicom-p10-downloader",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 });
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
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: 'OHIFExtDicomSeg',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
libraryExport: 'default',
|
||||||
|
filename: pkg.main,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## 0.0.2 (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# @ohif/extension-dicom-rt
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-dicom-rt",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"main": "dist/index.umd.js",
|
||||||
|
"module": "src/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||||
|
"dev:dicom-html": "yarn run dev",
|
||||||
|
"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": "^0.50.0",
|
||||||
|
"cornerstone-core": "^2.2.8",
|
||||||
|
"cornerstone-tools": "^4.0.9",
|
||||||
|
"dcmjs": "^0.8.3",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"react-select": "^3.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
|
||||||
|
import loadRTStruct from './loadRTStruct';
|
||||||
|
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
|
// TODO: Should probably use dcmjs for this
|
||||||
|
const SOP_CLASS_UIDS = {
|
||||||
|
DICOM_RT_STRUCT: '1.2.840.10008.5.1.4.1.1.481.3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||||
|
|
||||||
|
const OHIFDicomRTStructSopClassHandler = {
|
||||||
|
id: 'OHIFDicomRTStructSopClassHandler',
|
||||||
|
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||||
|
sopClassUIDs,
|
||||||
|
getDisplaySetFromSeries: function (
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
dicomWebClient,
|
||||||
|
authorizationHeaders
|
||||||
|
) {
|
||||||
|
const instance = series.getFirstInstance();
|
||||||
|
|
||||||
|
const metadata = instance.getData().metadata;
|
||||||
|
const {
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = metadata;
|
||||||
|
|
||||||
|
// TODO -> GET REFERENCED FRAME OF REFERENCE SEQUENCE.
|
||||||
|
|
||||||
|
const rtStructDisplaySet = {
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
wadoRoot: study.getData().wadoRoot,
|
||||||
|
wadoUri: instance.getData().wadouri,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
authorizationHeaders,
|
||||||
|
metadata,
|
||||||
|
isDerived: true,
|
||||||
|
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||||
|
labelmapIndex: null, // Assigned when loaded.
|
||||||
|
isLoaded: false,
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!metadata.ReferencedSeriesSequence) {
|
||||||
|
const ReferencedFrameOfReferenceSequence =
|
||||||
|
metadata.ReferencedFrameOfReferenceSequence;
|
||||||
|
|
||||||
|
if (ReferencedFrameOfReferenceSequence) {
|
||||||
|
// TODO -> @dannyrb Do we augment metadata or add a (potentially large? fallback list in filterDerivedDisplaySets )
|
||||||
|
metadata.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||||
|
return loadRTStruct(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
studies
|
||||||
|
).catch(error => {
|
||||||
|
rtStructDisplaySet.isLoaded = false;
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return rtStructDisplaySet;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
) {
|
||||||
|
const ReferencedSeriesSequence = [];
|
||||||
|
|
||||||
|
_getSequenceAsArray(ReferencedFrameOfReferenceSequence).forEach(
|
||||||
|
referencedFrameOfReference => {
|
||||||
|
const { RTReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
_getSequenceAsArray(RTReferencedStudySequence).forEach(
|
||||||
|
rtReferencedStudy => {
|
||||||
|
const { RTReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
_getSequenceAsArray(RTReferencedSeriesSequence).forEach(
|
||||||
|
rtReferencedSeries => {
|
||||||
|
const ReferencedInstanceSequence = [];
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
} = rtReferencedSeries;
|
||||||
|
|
||||||
|
_getSequenceAsArray(ContourImageSequence).forEach(
|
||||||
|
contourImage => {
|
||||||
|
ReferencedInstanceSequence.push({
|
||||||
|
ReferencedSOPInstanceUID:
|
||||||
|
contourImage.ReferencedSOPInstanceUID,
|
||||||
|
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSeries = {
|
||||||
|
SeriesInstanceUID,
|
||||||
|
ReferencedInstanceSequence,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReferencedSeriesSequence.push(referencedSeries);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return ReferencedSeriesSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getSequenceAsArray(sequence) {
|
||||||
|
return Array.isArray(sequence) ? sequence : [sequence];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getReferencedFrameOfReferenceSequence(instance) {
|
||||||
|
const referencedFrameOfReferenceSequenceRaw = instance['30060010'];
|
||||||
|
|
||||||
|
const referencedFrameOfReferenceSequence = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!referencedFrameOfReferenceSequenceRaw ||
|
||||||
|
!referencedFrameOfReferenceSequenceRaw.Value
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
referencedFrameOfReferenceSequenceRaw.Value.forEach(
|
||||||
|
referencedFrameOfReferenceRaw => {
|
||||||
|
const frameOfReferenceUID = DICOMWeb.getString(
|
||||||
|
referencedFrameOfReferenceRaw['00200052']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedFrameOfReference = { frameOfReferenceUID };
|
||||||
|
const RTReferencedStudySequenceRaw =
|
||||||
|
referencedFrameOfReferenceRaw['30060012'];
|
||||||
|
|
||||||
|
referencedFrameOfReferenceSequence.push(referencedFrameOfReference);
|
||||||
|
|
||||||
|
if (RTReferencedStudySequenceRaw && RTReferencedStudySequenceRaw.Value) {
|
||||||
|
referencedFrameOfReference.rtReferencedStudySequence = [];
|
||||||
|
|
||||||
|
const { rtReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
RTReferencedStudySequenceRaw.Value.forEach(rtReferencedStudyRaw => {
|
||||||
|
const referencedSopClassUID = DICOMWeb.getString(
|
||||||
|
rtReferencedStudyRaw['00081150']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||||
|
rtReferencedStudyRaw['00081155']
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtReferencedStudy = {
|
||||||
|
referencedSopClassUID,
|
||||||
|
referencedSOPInstanceUID,
|
||||||
|
rtReferencedSeriesSequence: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rtReferencedStudySequence.push(rtReferencedStudy);
|
||||||
|
|
||||||
|
const { rtReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
const rtReferencedSeriesSequenceRaw =
|
||||||
|
rtReferencedStudyRaw['30060014'];
|
||||||
|
|
||||||
|
rtReferencedSeriesSequenceRaw.Value.forEach(rtReferencedSeriesRaw => {
|
||||||
|
const seriesInstanceUID = DICOMWeb.getString(
|
||||||
|
rtReferencedSeriesRaw['0020000E']
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtReferencedSeries = {
|
||||||
|
seriesInstanceUID,
|
||||||
|
contourImageSequence: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rtReferencedSeriesSequence.push(rtReferencedSeries);
|
||||||
|
|
||||||
|
const { contourImageSequence } = rtReferencedSeries;
|
||||||
|
|
||||||
|
const contourImageSequenceRaw = rtReferencedSeriesRaw['30060016'];
|
||||||
|
|
||||||
|
contourImageSequenceRaw.Value.forEach(contourImageRaw => {
|
||||||
|
const referencedSOPClassUID = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081150']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081155']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedFrameNumber = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081160']
|
||||||
|
);
|
||||||
|
const referencedSegmentNumber = DICOMWeb.getString(
|
||||||
|
contourImageRaw['0062000B']
|
||||||
|
);
|
||||||
|
|
||||||
|
const contourImage = {
|
||||||
|
referencedSOPClassUID,
|
||||||
|
referencedSOPInstanceUID,
|
||||||
|
referencedFrameNumber,
|
||||||
|
referencedSegmentNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
contourImageSequence.push(contourImage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return referencedFrameOfReferenceSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OHIFDicomRTStructSopClassHandler;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
|
||||||
|
return (
|
||||||
|
<div style={expand ? { height: "100%", width: "100%" } : {}}>
|
||||||
|
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||||
|
<g transform="rotate(0 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(30 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(60 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(90 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(120 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(150 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(180 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(210 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(240 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(270 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(300 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(330 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingIndicator;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.dcmrt-panel-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon,
|
||||||
|
.dcmrt-panel-section .header .angle-double-down,
|
||||||
|
.dcmrt-panel-section .header .angle-double-up {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon:hover,
|
||||||
|
.dcmrt-panel-section .header .angle-double-down:hover,
|
||||||
|
.dcmrt-panel-section .header .angle-double-up:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon.expanded {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons .angle-double-down, .icons .angle-double-up {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './PanelSection.css';
|
||||||
|
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||||
|
|
||||||
|
const PanelSection = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
visible = false,
|
||||||
|
expanded = false,
|
||||||
|
loading = false,
|
||||||
|
hideVisibleButton = false,
|
||||||
|
onVisibilityChange = () => { },
|
||||||
|
onExpandChange = () => { }
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||||
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(visible);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="dcmrt-panel-section"
|
||||||
|
style={{
|
||||||
|
marginBottom: isExpanded ? 0 : 2,
|
||||||
|
height: isExpanded ? '100%' : 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="header">
|
||||||
|
<div>{title}</div>
|
||||||
|
<div className="icons">
|
||||||
|
{!hideVisibleButton && (
|
||||||
|
<Icon
|
||||||
|
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||||
|
name={isVisible ? "eye" : "eye-closed"}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={() => {
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Icon
|
||||||
|
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
|
||||||
|
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={() => {
|
||||||
|
const newExpandValue = !isExpanded;
|
||||||
|
setIsExpanded(newExpandValue);
|
||||||
|
onExpandChange(newExpandValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelSection.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
expanded: PropTypes.bool,
|
||||||
|
onVisibilityChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelSection.defaultProps = {
|
||||||
|
visible: false,
|
||||||
|
expanded: false,
|
||||||
|
onVisibilityChange: () => { }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PanelSection;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.dcmrt-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header .cog-icon {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: var(--default-color);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
|
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import './RTPanel.css';
|
||||||
|
import StructureSetItem from '../StructureSetItem/StructureSetItem';
|
||||||
|
import RTPanelSettings from '../RTSettings/RTSettings';
|
||||||
|
import PanelSection from '../PanelSection/PanelSection';
|
||||||
|
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||||
|
|
||||||
|
const { studyMetadataManager } = utils;
|
||||||
|
|
||||||
|
const refreshViewport = () => {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RTPanel component
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.studies
|
||||||
|
* @param {Array} props.viewports - viewportSpecificData
|
||||||
|
* @param {number} props.activeIndex - activeViewportIndex
|
||||||
|
* @param {number} props.isOpen - isOpen
|
||||||
|
* @returns component
|
||||||
|
*/
|
||||||
|
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
|
||||||
|
const [selectedContour, setSelectedContour] = useState();
|
||||||
|
const DEFAULT_SET_INDEX = 0;
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
referencedDisplaysets: [],
|
||||||
|
sets: [],
|
||||||
|
selectedSet: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [state, setState] = useState(DEFAULT_STATE);
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const activeViewport = viewports[activeIndex];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||||
|
* or other components that could rely on loaded rtstructs that
|
||||||
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new structs.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||||
|
* updated after the structs are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateStructureSets = () => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const StructureSets = module.state.StructureSets;
|
||||||
|
|
||||||
|
if (StructureSets && StructureSets.length) {
|
||||||
|
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||||
|
activeViewport.SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
||||||
|
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||||
|
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (viewportSets.length) {
|
||||||
|
const defaultSet = viewportSets[DEFAULT_SET_INDEX];
|
||||||
|
setState({
|
||||||
|
referencedDisplaysets,
|
||||||
|
selectedSet: defaultSet,
|
||||||
|
sets: viewportSets
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(DEFAULT_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateStructureSets();
|
||||||
|
}, [studies, viewports, activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowSettings(showSettings && !isOpen);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const toContourItem = ({ ROINumber, ROIName, RTROIObservations, colorArray, visible }, loadedSet) => {
|
||||||
|
let interpretedType = '';
|
||||||
|
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
|
||||||
|
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameContour = selectedContour && selectedContour === ROINumber;
|
||||||
|
return (
|
||||||
|
<StructureSetItem
|
||||||
|
key={ROINumber}
|
||||||
|
selected={isSameContour}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedContour(isSameContour ? null : ROINumber);
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
const element = enabledElements[activeIndex].element;
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = toolState.data[0].imageIds;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
||||||
|
state.selectedSet.SeriesInstanceUID,
|
||||||
|
ROINumber,
|
||||||
|
imageIds
|
||||||
|
);
|
||||||
|
|
||||||
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
||||||
|
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
||||||
|
|
||||||
|
onContourItemClick({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex: activeIndex
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
label={`${ROIName} ${interpretedType}`}
|
||||||
|
index={ROINumber}
|
||||||
|
color={colorArray}
|
||||||
|
visible={visible}
|
||||||
|
onVisibilityChange={() => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.setters.toggleROIContour(state.selectedSet.SeriesInstanceUID, ROINumber);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configurationChangeHandler = newConfiguration => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.configuration.lineWidth = newConfiguration.lineWidth;
|
||||||
|
module.configuration.opacity = newConfiguration.opacity;
|
||||||
|
refreshViewport();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showSettings) {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
return (
|
||||||
|
<RTPanelSettings
|
||||||
|
configuration={module.configuration}
|
||||||
|
onBack={() => setShowSettings(false)}
|
||||||
|
onChange={configurationChangeHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dcmrt-panel">
|
||||||
|
<div className="dcmrt-panel-header">
|
||||||
|
<h3>RT Structure Sets</h3>
|
||||||
|
<Icon
|
||||||
|
className="cog-icon"
|
||||||
|
name="cog"
|
||||||
|
width="25px"
|
||||||
|
height="25px"
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
|
{state.referencedDisplaysets.map(displaySet => {
|
||||||
|
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
|
||||||
|
const loadedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||||
|
return (
|
||||||
|
<PanelSection
|
||||||
|
key={SeriesInstanceUID}
|
||||||
|
title={metadata.StructureSetLabel}
|
||||||
|
loading={!isLoaded || !loadedSet}
|
||||||
|
visible={isLoaded && loadedSet.visible}
|
||||||
|
hideVisibleButton={!isLoaded}
|
||||||
|
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
||||||
|
onVisibilityChange={() => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.setters.toggleStructureSet(SeriesInstanceUID);
|
||||||
|
}}
|
||||||
|
onExpandChange={async () => {
|
||||||
|
if (!isLoaded) {
|
||||||
|
await displaySet.load(viewports[activeIndex], studies);
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
const selectedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||||
|
setState(state => ({ ...state, selectedSet, sets }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollableArea>
|
||||||
|
<TableList headless>
|
||||||
|
{isLoaded && loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
|
||||||
|
</TableList>
|
||||||
|
</ScrollableArea>
|
||||||
|
</PanelSection>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RTPanel.propTypes = {
|
||||||
|
viewports: PropTypes.shape({
|
||||||
|
displaySetInstanceUID: PropTypes.string,
|
||||||
|
frameRate: PropTypes.any,
|
||||||
|
InstanceNumber: PropTypes.number,
|
||||||
|
isMultiFrame: PropTypes.bool,
|
||||||
|
isReconstructable: PropTypes.bool,
|
||||||
|
Modality: PropTypes.string,
|
||||||
|
plugin: PropTypes.string,
|
||||||
|
SeriesDate: PropTypes.string,
|
||||||
|
SeriesDescription: PropTypes.string,
|
||||||
|
SeriesInstanceUID: PropTypes.string,
|
||||||
|
SeriesNumber: PropTypes.any,
|
||||||
|
SeriesTime: PropTypes.string,
|
||||||
|
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
StudyInstanceUID: PropTypes.string,
|
||||||
|
}),
|
||||||
|
activeIndex: PropTypes.number.isRequired,
|
||||||
|
studies: PropTypes.array.isRequired,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
RTPanel.defaultProps = {};
|
||||||
|
|
||||||
|
export default RTPanel;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
.dcmrt-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-title h3 {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section .content {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section .header {
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range:not(:last-of-type) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range label {
|
||||||
|
margin: 0;
|
||||||
|
display: unset;
|
||||||
|
min-width: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .return-button {
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 4px 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .return-button:hover,
|
||||||
|
.dcmrt-settings .return-button:active,
|
||||||
|
.dcmrt-settings .return-button.active {
|
||||||
|
background-color: var(--active-color);
|
||||||
|
border-color: var(--ui-border-color-active);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Range } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './RTSettings.css';
|
||||||
|
|
||||||
|
const RTSettings = ({ configuration, onBack, onChange }) => {
|
||||||
|
const toFloat = value => parseFloat(value / 100).toFixed(2);
|
||||||
|
|
||||||
|
const save = (field, value) => {
|
||||||
|
onChange({ ...configuration, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsSection = ({ title, children }) => {
|
||||||
|
return (
|
||||||
|
<div className="settings-section">
|
||||||
|
<div className="header">{title}</div>
|
||||||
|
<div className="content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dcmrt-settings">
|
||||||
|
<div className="settings-title">
|
||||||
|
<h3>RT Structure Set Settings</h3>
|
||||||
|
<button className="return-button" onClick={onBack}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsSection title="Segment Outline">
|
||||||
|
<div className="range">
|
||||||
|
<label htmlFor="range">Opacity</label>
|
||||||
|
<Range
|
||||||
|
showPercentage
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={configuration.opacity * 100}
|
||||||
|
onChange={event => save('opacity', toFloat(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="range">
|
||||||
|
<label htmlFor="range">Width</label>
|
||||||
|
<Range
|
||||||
|
showValue
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
value={configuration.lineWidth}
|
||||||
|
onChange={event => save('lineWidth', parseInt(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RTSettings.propTypes = {
|
||||||
|
configuration: PropTypes.shape({
|
||||||
|
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
onBack: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RTSettings;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
.dcmrt-structure-set-item .item-color-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-color {
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-info {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 9px;
|
||||||
|
padding-left: 9px;
|
||||||
|
width: 90px;
|
||||||
|
border-left: 1px solid var(--text-secondary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item.selected .item-actions {
|
||||||
|
height: 35px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions {
|
||||||
|
margin-left: -1px;
|
||||||
|
background-color: var(--ui-gray-darker);
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
visibility: hidden;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--default-color);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 35px;
|
||||||
|
height: 35px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction:hover, .dcmrt-structure-set-item .item-actions .btnAction:active {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item.selected .item-actions {
|
||||||
|
height: 35px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: calc(100% - 15px); /* calc(100% - 50px); 20px = eye icon */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon.--visible {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './StructureSetItem.css';
|
||||||
|
|
||||||
|
const ColoredCircle = ({ color }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="item-color"
|
||||||
|
style={{ backgroundColor: `rgba(${color.join(',')})` }}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ColoredCircle.propTypes = {
|
||||||
|
color: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StructureSetItem = ({
|
||||||
|
index,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
itemClass,
|
||||||
|
color,
|
||||||
|
visible = true,
|
||||||
|
onVisibilityChange,
|
||||||
|
selected = false,
|
||||||
|
}) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
return (
|
||||||
|
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
||||||
|
<TableListItem
|
||||||
|
key={index}
|
||||||
|
itemKey={index}
|
||||||
|
itemIndex={index}
|
||||||
|
itemClass={itemClass}
|
||||||
|
itemMeta={<ColoredCircle color={color} />}
|
||||||
|
itemMetaClass="item-color-section"
|
||||||
|
onItemClick={onClick}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||||
|
{label}
|
||||||
|
<Icon
|
||||||
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{false && <div className="item-info">{'...'}</div>}
|
||||||
|
{false && (
|
||||||
|
<div className="item-actions">
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Relabelling...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
|
Relabel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Editing description...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
|
Description
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableListItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StructureSetItem.propTypes = {
|
||||||
|
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
itemClass: PropTypes.string,
|
||||||
|
color: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
StructureSetItem.defaultProps = {
|
||||||
|
itemClass: '',
|
||||||
|
onClick: () => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StructureSetItem;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const id = 'rt';
|
||||||
|
|
||||||
|
export default id;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import init from './init.js';
|
||||||
|
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||||
|
import id from './id.js';
|
||||||
|
import RTPanel from './components/RTPanel/RTPanel';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
|
init({ servicesManager, configuration });
|
||||||
|
},
|
||||||
|
getPanelModule({ commandsManager }) {
|
||||||
|
const ExtendedRTPanel = props => {
|
||||||
|
const contourItemClickHandler = contourData => {
|
||||||
|
commandsManager.runCommand('jumpToImage', contourData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RTPanel
|
||||||
|
{...props}
|
||||||
|
onContourItemClick={contourItemClickHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
icon: 'list',
|
||||||
|
label: 'RTSTRUCT',
|
||||||
|
target: 'rt-panel',
|
||||||
|
isDisabled: studies => {
|
||||||
|
if (!studies) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < studies.length; i++) {
|
||||||
|
const study = studies[i];
|
||||||
|
|
||||||
|
if (study && study.series) {
|
||||||
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
|
const series = study.series[j];
|
||||||
|
console.log(studies);
|
||||||
|
if (['RTSTRUCT', 'RTPLAN', 'RTDOSE'].includes(series.Modality)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'rt-panel',
|
||||||
|
component: ExtendedRTPanel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultContext: ['VIEWER'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getSopClassHandlerModule({ servicesManager }) {
|
||||||
|
return sopClassHandlerModule;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import RTStructDisplayTool from './tools/RTStructDisplayTool';
|
||||||
|
import rtStructModule from './tools/modules/rtStructModule';
|
||||||
|
|
||||||
|
import TOOL_NAMES from './utils/toolNames';
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
TOOL_NAMES: {
|
||||||
|
RT_STRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} configuration
|
||||||
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
|
*/
|
||||||
|
export default function init({ servicesManager, configuration = {} }) {
|
||||||
|
const conifg = Object.assign({}, defaultConfig, configuration);
|
||||||
|
|
||||||
|
TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL = conifg.TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
cornerstoneTools.register('module', 'rtstruct', rtStructModule);
|
||||||
|
cornerstoneTools.addTool(RTStructDisplayTool);
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||||
|
import TOOL_NAMES from './utils/toolNames';
|
||||||
|
|
||||||
|
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
const { DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
|
export default async function loadRTStruct(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
studies
|
||||||
|
) {
|
||||||
|
const rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||||
|
|
||||||
|
// Set here is loading is asynchronous.
|
||||||
|
// If this function throws its set back to false.
|
||||||
|
rtStructDisplaySet.isLoaded = true;
|
||||||
|
|
||||||
|
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||||
|
|
||||||
|
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
studies
|
||||||
|
);
|
||||||
|
|
||||||
|
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||||
|
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
|
dicomData.dict
|
||||||
|
);
|
||||||
|
|
||||||
|
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
||||||
|
dicomData.meta
|
||||||
|
);
|
||||||
|
|
||||||
|
// global cornerstone tools state to attach measurements to.
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
StructureSetROISequence,
|
||||||
|
ROIContourSequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
StructureSetLabel,
|
||||||
|
} = rtStructDataset;
|
||||||
|
|
||||||
|
// Define our structure set entry and add it to the rtstruct module state.
|
||||||
|
const structureSet = {
|
||||||
|
StructureSetLabel,
|
||||||
|
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
|
||||||
|
ROIContours: [],
|
||||||
|
referencedSeriesSequence:
|
||||||
|
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
rtStructModule.setters.structureSet(structureSet);
|
||||||
|
|
||||||
|
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||||
|
|
||||||
|
const ROIContour = ROIContourSequence[i];
|
||||||
|
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
||||||
|
|
||||||
|
if (!ContourSequence) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let c = 0; c < ContourSequence.length; c++) {
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
ContourData,
|
||||||
|
NumberOfContourPoints,
|
||||||
|
ContourGeometricType,
|
||||||
|
} = ContourSequence[c];
|
||||||
|
|
||||||
|
if (ContourGeometricType !== 'CLOSED_PLANAR') {
|
||||||
|
// TODO: Do we want to visualise types other than closed planar?
|
||||||
|
// We could easily do open planar.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||||
|
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||||
|
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||||
|
toolState,
|
||||||
|
imageId,
|
||||||
|
rtStructDisplayToolName
|
||||||
|
);
|
||||||
|
|
||||||
|
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||||
|
points.push({
|
||||||
|
x: ContourData[p],
|
||||||
|
y: ContourData[p + 1],
|
||||||
|
z: ContourData[p + 2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transformPointsToImagePlane(points, imagePlane);
|
||||||
|
|
||||||
|
const measurementData = {
|
||||||
|
handles: {
|
||||||
|
points,
|
||||||
|
},
|
||||||
|
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
||||||
|
ROINumber: ReferencedROINumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
imageIdSpecificToolData.push(measurementData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||||
|
* or other components that could rely on loaded rtstructs that
|
||||||
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new structs.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||||
|
* updated after the structs are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour
|
||||||
|
) {
|
||||||
|
const StructureSetROI = StructureSetROISequence.find(
|
||||||
|
structureSetROI =>
|
||||||
|
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const ROIContourData = {
|
||||||
|
ROINumber: StructureSetROI.ROINumber,
|
||||||
|
ROIName: StructureSetROI.ROIName,
|
||||||
|
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||||
|
ROIDescription: StructureSetROI.ROIDescription,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
_setROIContourDataColor(ROIContour, ROIContourData);
|
||||||
|
|
||||||
|
if (RTROIObservationsSequence) {
|
||||||
|
// If present, add additional RTROIObservations metadata.
|
||||||
|
_setROIContourRTROIObservations(
|
||||||
|
ROIContourData,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
structureSet.ROIContours.push(ROIContourData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourDataColor(ROIContour, ROIContourData) {
|
||||||
|
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
|
||||||
|
|
||||||
|
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
|
||||||
|
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
|
||||||
|
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ROIDisplayColor) {
|
||||||
|
ROIContourData.colorArray = [...ROIDisplayColor];
|
||||||
|
} else {
|
||||||
|
//Choose a color from the cornerstoneTools colorLUT
|
||||||
|
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
|
||||||
|
const { getters } = cornerstoneTools.getModule('segmentation');
|
||||||
|
const color = getters.colorForSegmentIndexColorLUT(
|
||||||
|
0,
|
||||||
|
ROIContourData.ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
ROIContourData.colorArray = [...color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourRTROIObservations(
|
||||||
|
ROIContourData,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROINumber
|
||||||
|
) {
|
||||||
|
const RTROIObservations = RTROIObservationsSequence.find(
|
||||||
|
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (RTROIObservations) {
|
||||||
|
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
||||||
|
const {
|
||||||
|
ObservationNumber,
|
||||||
|
ROIObservationDescription,
|
||||||
|
RTROIInterpretedType,
|
||||||
|
ROIInterpreter,
|
||||||
|
} = RTROIObservations;
|
||||||
|
|
||||||
|
ROIContourData.RTROIObservations = {
|
||||||
|
ObservationNumber,
|
||||||
|
ROIObservationDescription,
|
||||||
|
RTROIInterpretedType,
|
||||||
|
ROIInterpreter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setToolEnabledIfNotEnabled(toolName) {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
const { element } = enabledElement;
|
||||||
|
const tool = cornerstoneTools.getToolForElement(element, toolName);
|
||||||
|
|
||||||
|
if (tool.mode !== 'enabled') {
|
||||||
|
// If not already active or passive, set passive so contours render.
|
||||||
|
cornerstoneTools.setToolEnabled(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
|
||||||
|
if (toolState.hasOwnProperty(imageId) === false) {
|
||||||
|
toolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdToolState = toolState[imageId];
|
||||||
|
|
||||||
|
// If we don't have tool state for this type of tool, add an empty object
|
||||||
|
if (imageIdToolState.hasOwnProperty(toolName) === false) {
|
||||||
|
imageIdToolState[toolName] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageIdToolState[toolName].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||||
|
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||||
|
imageIdSopInstanceUidPairsEntry =>
|
||||||
|
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
return imageIdSopInstanceUidPairsEntry.imageId;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const study = studies.find(
|
||||||
|
study => study.StudyInstanceUID === StudyInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const displaySets = study.displaySets.filter(set => {
|
||||||
|
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (displaySets.length > 1) {
|
||||||
|
console.warn(
|
||||||
|
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||||
|
);
|
||||||
|
// TODO -> We could make check the instance list and see if any match?
|
||||||
|
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||||
|
// ^ Will that even happen?
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = displaySets[0];
|
||||||
|
|
||||||
|
return referencedDisplaySet.images.map(image => {
|
||||||
|
return {
|
||||||
|
imageId: image.getImageId(),
|
||||||
|
sopInstanceUID: image.getSOPInstanceUID(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||||
|
|
||||||
|
import TOOL_NAMES from '../utils/toolNames';
|
||||||
|
|
||||||
|
// Cornerstone 3rd party dev kit imports
|
||||||
|
const draw = importInternal('drawing/draw');
|
||||||
|
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||||
|
const getNewContext = importInternal('drawing/getNewContext');
|
||||||
|
const BaseTool = importInternal('base/BaseTool');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay).
|
||||||
|
* @extends cornerstoneTools.BaseTool
|
||||||
|
*/
|
||||||
|
export default class RTStructDisplayTool extends BaseTool {
|
||||||
|
constructor(props = {}) {
|
||||||
|
const defaultProps = {
|
||||||
|
mixins: ['enabledOrDisabledBinaryTool'],
|
||||||
|
name: TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialProps = Object.assign(defaultProps, props);
|
||||||
|
|
||||||
|
super(initialProps);
|
||||||
|
|
||||||
|
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolData(evt) {
|
||||||
|
const eventData = evt.detail;
|
||||||
|
const rtstructModule = this._rtStructModule;
|
||||||
|
|
||||||
|
const toolState = getToolState(evt.currentTarget, this.name);
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lineWidth, opacity } = rtstructModule.configuration;
|
||||||
|
|
||||||
|
// We have tool data for this element - iterate over each one and draw it
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
for (let i = 0; i < toolState.data.length; i++) {
|
||||||
|
const data = toolState.data[i];
|
||||||
|
|
||||||
|
const structureSet = rtstructModule.getters.structureSet(
|
||||||
|
data.structureSetSeriesInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if entire StructureSet is hidden.
|
||||||
|
if (structureSet.visible === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROIContourData = rtstructModule.getters.ROIContour(
|
||||||
|
data.structureSetSeriesInstanceUid,
|
||||||
|
data.ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if ROIContour is hidden.
|
||||||
|
if (ROIContourData.visible === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = data.handles.points;
|
||||||
|
|
||||||
|
if (!points.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorArray = ROIContourData.colorArray;
|
||||||
|
const color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||||
|
colorArray[2]
|
||||||
|
},${opacity})`;
|
||||||
|
|
||||||
|
lineWidth;
|
||||||
|
|
||||||
|
draw(context, context => {
|
||||||
|
drawJoinedLines(
|
||||||
|
context,
|
||||||
|
eventData.element,
|
||||||
|
points[points.length - 1],
|
||||||
|
points,
|
||||||
|
{
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import TOOL_NAMES from '../../../utils/toolNames';
|
||||||
|
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getImageIdOfCenterFrameOfROIContour - Returns the imageId nearest to the center of the
|
||||||
|
* volumes extent in the stack's Z direction which contains one of the ROIContour's Contours.
|
||||||
|
* @param {string} structureSetSeriesInstanceUid The SeriesInstanceUID of the structure set.
|
||||||
|
* @param {number} ROINumber The ROINumber of the region.
|
||||||
|
* @param {string} imageIdsInStack The stack of imageIds.
|
||||||
|
*
|
||||||
|
* @returns The imageId
|
||||||
|
*/
|
||||||
|
export default function getImageIdOfCenterFrameOfROIContour(
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber,
|
||||||
|
imageIdsInStack
|
||||||
|
) {
|
||||||
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
const imageIdIndicies = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < imageIdsInStack.length; i++) {
|
||||||
|
const imageId = imageIdsInStack[i];
|
||||||
|
|
||||||
|
const imageIdSpecificToolState = toolState[imageId];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!imageIdSpecificToolState ||
|
||||||
|
!imageIdSpecificToolState[rtStructDisplayToolName] ||
|
||||||
|
!imageIdSpecificToolState[rtStructDisplayToolName].data
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdSpecificToolState[rtStructDisplayToolName].data;
|
||||||
|
|
||||||
|
if (
|
||||||
|
_toolDataContainsROIContour(
|
||||||
|
toolData,
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
imageIdIndicies.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerImageIdIndex =
|
||||||
|
imageIdIndicies[Math.floor(imageIdIndicies.length / 2)];
|
||||||
|
|
||||||
|
return imageIdsInStack[centerImageIdIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _toolDataContainsROIContour(
|
||||||
|
toolData,
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber
|
||||||
|
) {
|
||||||
|
return !!toolData.some(
|
||||||
|
toolDataI =>
|
||||||
|
toolDataI.structureSetSeriesInstanceUid ===
|
||||||
|
structureSetSeriesInstanceUid && toolDataI.ROINumber === ROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* structureSetReferencesSeriesInstanceUid - Returns true if the structure set
|
||||||
|
* references the given SeriesInstanceUID.
|
||||||
|
* @param {*} StructureSet
|
||||||
|
* @param {*} SeriesInstanceUID
|
||||||
|
*/
|
||||||
|
export default function structureSetReferencesSeriesInstanceUid(
|
||||||
|
StructureSet,
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const { referencedSeriesSequence } = StructureSet;
|
||||||
|
return referencedSeriesSequence.some(
|
||||||
|
referencedSeries =>
|
||||||
|
referencedSeries.SeriesInstanceUID ===
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import getImageIdOfCenterFrameOfROIContour from './lib/getImageIdOfCenterFrameOfROIContour';
|
||||||
|
import structureSetReferencesSeriesInstanceUid from './lib/structureSetReferencesSeriesInstanceUid';
|
||||||
|
|
||||||
|
// We should put this as a helper somewhere as we are using it in mutliple places.
|
||||||
|
function refreshViewport() {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration = {
|
||||||
|
lineWidth: 3,
|
||||||
|
opacity: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
StructureSets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a structure set to the module.
|
||||||
|
* @param {Object} structureSetData The structure set data.
|
||||||
|
*/
|
||||||
|
function setStructureSet(structureSetData) {
|
||||||
|
state.StructureSets.push(structureSetData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the StructureSet with the given SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*
|
||||||
|
* @returns {Object} The StructureSet.
|
||||||
|
*/
|
||||||
|
function getStructureSet(SeriesInstanceUID) {
|
||||||
|
return state.StructureSets.find(
|
||||||
|
structureSet => structureSet.SeriesInstanceUID === SeriesInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ROI Contour with the given ROINumber on the StructureSet defined by the given
|
||||||
|
* SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour to fetch.
|
||||||
|
*/
|
||||||
|
function getROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
const structureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (!structureSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return structureSet.ROIContours.find(
|
||||||
|
ROIContour => ROIContour.ROINumber === ROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setHideStructureSet(SeriesInstanceUID) {
|
||||||
|
_setStructureSetVisible(SeriesInstanceUID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setShowStructureSet(SeriesInstanceUID) {
|
||||||
|
_setStructureSetVisible(SeriesInstanceUID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||||
|
*/
|
||||||
|
function _setStructureSetVisible(SeriesInstanceUID, visible = true) {
|
||||||
|
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (StructureSet) {
|
||||||
|
StructureSet.visible = visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setToggleStructureSet(SeriesInstanceUID) {
|
||||||
|
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (StructureSet) {
|
||||||
|
StructureSet.visible = !StructureSet.visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setHideROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
_setROIContourVisible(SeriesInstanceUID, ROINumber, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setShowROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
_setROIContourVisible(SeriesInstanceUID, ROINumber, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||||
|
*/
|
||||||
|
function _setROIContourVisible(SeriesInstanceUID, ROINumber, visible = true) {
|
||||||
|
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||||
|
|
||||||
|
if (ROIContour) {
|
||||||
|
ROIContour.visible = visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setToggleROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||||
|
|
||||||
|
if (ROIContour) {
|
||||||
|
ROIContour.visible = !ROIContour.visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of StructureSets which reference the given SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID to check.
|
||||||
|
*/
|
||||||
|
function getStructuresSetsWhichReferenceSeriesInstanceUid(
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const { StructureSets } = state;
|
||||||
|
return StructureSets.filter(StructureSet =>
|
||||||
|
structureSetReferencesSeriesInstanceUid(
|
||||||
|
StructureSet,
|
||||||
|
SeriesInstanceUID
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getters: {
|
||||||
|
structureSet: getStructureSet,
|
||||||
|
ROIContour: getROIContour,
|
||||||
|
structuresSetsWhichReferenceSeriesInstanceUid: getStructuresSetsWhichReferenceSeriesInstanceUid,
|
||||||
|
imageIdOfCenterFrameOfROIContour: getImageIdOfCenterFrameOfROIContour,
|
||||||
|
},
|
||||||
|
setters: {
|
||||||
|
structureSet: setStructureSet,
|
||||||
|
hideROIContour: setHideROIContour,
|
||||||
|
showROIContour: setShowROIContour,
|
||||||
|
toggleROIContour: setToggleROIContour,
|
||||||
|
hideStructureSet: setHideStructureSet,
|
||||||
|
showStructureSet: setShowStructureSet,
|
||||||
|
toggleStructureSet: setToggleStructureSet,
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
configuration,
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const TOOL_NAMES = {
|
||||||
|
RTSTRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOOL_NAMES;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
export default function transformPointsToImagePlane(points, imagePlane) {
|
||||||
|
// See Equation C.7.6.2.1-1 of the DICOM standard
|
||||||
|
|
||||||
|
const {
|
||||||
|
rowCosines,
|
||||||
|
columnCosines,
|
||||||
|
rowPixelSpacing: deltaI,
|
||||||
|
columnPixelSpacing: deltaJ,
|
||||||
|
imagePositionPatient,
|
||||||
|
} = imagePlane;
|
||||||
|
|
||||||
|
const X = [rowCosines[0], rowCosines[1], rowCosines[2]];
|
||||||
|
const Y = [columnCosines[0], columnCosines[1], columnCosines[2]];
|
||||||
|
const S = [
|
||||||
|
imagePositionPatient[0],
|
||||||
|
imagePositionPatient[1],
|
||||||
|
imagePositionPatient[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 9 sets of simulataneous equations to choose from, choose which set to solve
|
||||||
|
// Based on the largest component of each direction cosine.
|
||||||
|
// This avoids NaNs or floating point errors caused by dividing by very small numbers and ensures a safe mapping
|
||||||
|
// when mapping between planes that are close to orthogonal.
|
||||||
|
|
||||||
|
let ix = 0;
|
||||||
|
let iy = 0;
|
||||||
|
let largestDirectionCosineMagnitude = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the element with the largest magnitude in each direction cosine vector.
|
||||||
|
for (let i = 0; i < X.length; i++) {
|
||||||
|
if (Math.abs(X[i]) > largestDirectionCosineMagnitude.x) {
|
||||||
|
ix = i;
|
||||||
|
largestDirectionCosineMagnitude.x = Math.abs(X[i]);
|
||||||
|
}
|
||||||
|
if (Math.abs(Y[i]) > largestDirectionCosineMagnitude.y) {
|
||||||
|
iy = i;
|
||||||
|
largestDirectionCosineMagnitude.y = Math.abs(Y[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ci = {
|
||||||
|
// Index of max elements in X and Y
|
||||||
|
ix,
|
||||||
|
iy,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanity Check
|
||||||
|
const directionCosineMagnitude = {
|
||||||
|
x: Math.pow(X[0], 2) + Math.pow(X[1], 2) + Math.pow(X[2], 2),
|
||||||
|
y: Math.pow(Y[0], 2) + Math.pow(Y[1], 2) + Math.pow(Y[2], 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (directionCosineMagnitude.x < 0.99 || directionCosineMagnitude.y < 0.99) {
|
||||||
|
throw Error(
|
||||||
|
`Direction cosines do not sum to 1 in quadrature. There is likely a mistake in the DICOM metadata.` +
|
||||||
|
`directionCosineMagnitudes: ${directionCosineMagnitude.x}, ${directionCosineMagnitude.y}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in elements that won't change between points
|
||||||
|
const c = [undefined, Y[ci.ix], X[ci.ix], undefined, X[ci.iy], Y[ci.iy]];
|
||||||
|
|
||||||
|
for (let pointI = 0; pointI < points.length; pointI++) {
|
||||||
|
// Subtract imagePositionPatient from the coordinate
|
||||||
|
const r = [
|
||||||
|
points[pointI].x - S[0],
|
||||||
|
points[pointI].y - S[1],
|
||||||
|
points[pointI].z - S[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set the variable terms in c.
|
||||||
|
c[0] = r[ci.ix];
|
||||||
|
c[3] = r[ci.iy];
|
||||||
|
|
||||||
|
// General case: Solves the two choosen simulataneous equations to go from the patient coordinate system to the imagePlane.
|
||||||
|
const i =
|
||||||
|
(c[0] - (c[1] * c[3]) / c[5]) /
|
||||||
|
(c[2] * deltaI * (1 - (c[1] * c[4]) / (c[2] * c[5])));
|
||||||
|
const j = (c[3] - c[4] * i * deltaI) / (c[5] * deltaJ);
|
||||||
|
|
||||||
|
// NOTE: Add (0.5, 0.5) to the coordinate, as PCS reference frame is with respect to the centre of the first pixel.
|
||||||
|
points[pointI].x = i + 0.5;
|
||||||
|
points[pointI].y = j + 0.5;
|
||||||
|
delete points[pointI].z;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.1...@ohif/extension-dicom-segmentation@0.1.2) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.0...@ohif/extension-dicom-segmentation@0.1.1) (2020-04-01)
|
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.0...@ohif/extension-dicom-segmentation@0.1.1) (2020-04-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-segmentation",
|
"name": "@ohif/extension-dicom-segmentation",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ SegmentationPanel.propTypes = {
|
|||||||
}),
|
}),
|
||||||
activeIndex: PropTypes.number.isRequired,
|
activeIndex: PropTypes.number.isRequired,
|
||||||
studies: PropTypes.array.isRequired,
|
studies: PropTypes.array.isRequired,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
SegmentationPanel.defaultProps = {};
|
SegmentationPanel.defaultProps = {};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,46 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.3.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.12...@ohif/extension-vtk@1.3.13) (2020-04-17)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.11...@ohif/extension-vtk@1.3.12) (2020-04-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.10...@ohif/extension-vtk@1.3.11) (2020-04-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.9...@ohif/extension-vtk@1.3.10) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-vtk
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "1.3.9",
|
"version": "1.3.13",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"react-vtkjs-viewport": "^0.8.3"
|
"react-vtkjs-viewport": "^0.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.6.8",
|
"@ohif/core": "^2.7.0",
|
||||||
"@ohif/ui": "^1.4.2",
|
"@ohif/ui": "^1.4.3",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.12.5",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { getReactVtkjsViewport } from './utils/getReactVtkjsViewport';
|
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||||
import LoadingIndicator from './LoadingIndicator.js';
|
import LoadingIndicator from './LoadingIndicator.js';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
@@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onScroll: () => { },
|
onScroll: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = 'OHIFVTKViewport';
|
static id = 'OHIFVTKViewport';
|
||||||
@@ -133,8 +133,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
SOPClassUID,
|
SOPClassUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
frameIndex,
|
frameIndex
|
||||||
reactVtkjsViewport
|
|
||||||
) => {
|
) => {
|
||||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||||
studies,
|
studies,
|
||||||
@@ -145,10 +144,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
frameIndex
|
frameIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageDataObject = reactVtkjsViewport.getImageData(
|
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||||
stack.imageIds,
|
|
||||||
displaySetInstanceUID
|
|
||||||
);
|
|
||||||
let labelmapDataObject;
|
let labelmapDataObject;
|
||||||
let labelmapColorLUT;
|
let labelmapColorLUT;
|
||||||
|
|
||||||
@@ -261,9 +257,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
return volumeActor;
|
return volumeActor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setStateFromProps() {
|
setStateFromProps() {
|
||||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
|
||||||
|
|
||||||
const { studies, displaySet } = this.props.viewportData;
|
const { studies, displaySet } = this.props.viewportData;
|
||||||
const {
|
const {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
@@ -301,10 +295,8 @@ class OHIFVTKViewport extends Component {
|
|||||||
studies,
|
studies,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
sopClassUIDs[0],
|
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
frameIndex,
|
frameIndex
|
||||||
reactVtkjsViewport
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.imageDataObject = imageDataObject;
|
this.imageDataObject = imageDataObject;
|
||||||
@@ -325,7 +317,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
dataDetails,
|
dataDetails,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.loadProgressively(imageDataObject, reactVtkjsViewport);
|
this.loadProgressively(imageDataObject);
|
||||||
|
|
||||||
// TODO: There must be a better way to do this.
|
// TODO: There must be a better way to do this.
|
||||||
// We do this so that if all the data is available the react-vtkjs-viewport
|
// We do this so that if all the data is available the react-vtkjs-viewport
|
||||||
@@ -343,26 +335,26 @@ class OHIFVTKViewport extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
await this.setStateFromProps();
|
this.setStateFromProps();
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { displaySet } = this.props.viewportData;
|
const { displaySet } = this.props.viewportData;
|
||||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
displaySet.displaySetInstanceUID !==
|
displaySet.displaySetInstanceUID !==
|
||||||
prevDisplaySet.displaySetInstanceUID ||
|
prevDisplaySet.displaySetInstanceUID ||
|
||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||||
) {
|
) {
|
||||||
await this.setStateFromProps();
|
this.setStateFromProps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadProgressively(imageDataObject, reactVtkjsViewport) {
|
loadProgressively(imageDataObject) {
|
||||||
reactVtkjsViewport.loadImageData(imageDataObject);
|
loadImageData(imageDataObject);
|
||||||
|
|
||||||
const { isLoading, insertPixelDataPromises } = imageDataObject;
|
const { isLoading, insertPixelDataPromises } = imageDataObject;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
import React, { useEffect, useCallback } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
// import { View2D } from 'react-vtkjs-viewport';
|
import { View2D } from 'react-vtkjs-viewport';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import asyncComponent from './asyncComponent.js';
|
|
||||||
import { getReactVtkjsViewport } from './utils/getReactVtkjsViewport';
|
|
||||||
|
|
||||||
import './VTKViewport.css';
|
import './VTKViewport.css';
|
||||||
|
|
||||||
const View2D = asyncComponent(async () => {
|
|
||||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
|
||||||
return { default: reactVtkjsViewport.View2D };
|
|
||||||
});
|
|
||||||
|
|
||||||
const VTKViewport = props => {
|
const VTKViewport = props => {
|
||||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||||
|
|
||||||
@@ -59,7 +52,7 @@ VTKViewport.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
VTKViewport.defaultProps = {
|
VTKViewport.defaultProps = {
|
||||||
onScroll: () => { },
|
onScroll: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VTKViewport;
|
export default VTKViewport;
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import throttle from 'lodash.throttle';
|
import throttle from 'lodash.throttle';
|
||||||
import { getReactVtkjsViewport } from './utils/getReactVtkjsViewport';
|
import {
|
||||||
|
vtkInteractorStyleMPRCrosshairs,
|
||||||
|
vtkInteractorStyleMPRWindowLevel,
|
||||||
|
vtkInteractorStyleMPRRotate,
|
||||||
|
vtkSVGCrosshairsWidget,
|
||||||
|
} from 'react-vtkjs-viewport';
|
||||||
|
|
||||||
import setMPRLayout from './utils/setMPRLayout.js';
|
import setMPRLayout from './utils/setMPRLayout.js';
|
||||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||||
@@ -119,18 +124,16 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
|
|
||||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||||
},
|
},
|
||||||
enableRotateTool: async () => {
|
enableRotateTool: () => {
|
||||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRRotate.newInstance();
|
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||||
|
|
||||||
api.setInteractorStyle({ istyle });
|
api.setInteractorStyle({ istyle });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enableCrosshairsTool: async () => {
|
enableCrosshairsTool: () => {
|
||||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
|
||||||
apis.forEach((api, apiIndex) => {
|
apis.forEach((api, apiIndex) => {
|
||||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRCrosshairs.newInstance();
|
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||||
|
|
||||||
api.setInteractorStyle({
|
api.setInteractorStyle({
|
||||||
istyle,
|
istyle,
|
||||||
@@ -138,7 +141,7 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
enableLevelTool: async () => {
|
enableLevelTool: () => {
|
||||||
function updateVOI(apis, windowWidth, windowCenter) {
|
function updateVOI(apis, windowWidth, windowCenter) {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
api.updateVOI(windowWidth, windowCenter);
|
api.updateVOI(windowWidth, windowCenter);
|
||||||
@@ -159,9 +162,8 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRWindowLevel.newInstance();
|
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||||
|
|
||||||
api.setInteractorStyle({ istyle, callbacks });
|
api.setInteractorStyle({ istyle, callbacks });
|
||||||
});
|
});
|
||||||
@@ -258,15 +260,14 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add widgets and set default interactorStyle of each viewport.
|
// Add widgets and set default interactorStyle of each viewport.
|
||||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
|
||||||
apis.forEach((api, apiIndex) => {
|
apis.forEach((api, apiIndex) => {
|
||||||
api.addSVGWidget(
|
api.addSVGWidget(
|
||||||
reactVtkjsViewport.vtkSVGCrosshairsWidget.newInstance(),
|
vtkSVGCrosshairsWidget.newInstance(),
|
||||||
'crosshairsWidget'
|
'crosshairsWidget'
|
||||||
);
|
);
|
||||||
|
|
||||||
const uid = api.uid;
|
const uid = api.uid;
|
||||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRCrosshairs.newInstance();
|
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||||
|
|
||||||
api.setInteractorStyle({
|
api.setInteractorStyle({
|
||||||
istyle,
|
istyle,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export function getReactVtkjsViewport() {
|
|
||||||
return import(
|
|
||||||
/* webpackChunkName: "ReactVtkjsViewport" */ 'react-vtkjs-viewport'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,49 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.11...@ohif/core@2.7.0) (2020-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* set the authorization header for DICOMWeb requests if provided in query string ([#1646](https://github.com/OHIF/Viewers/issues/1646)) ([450c80b](https://github.com/OHIF/Viewers/commit/450c80b9d5f172be8b5713b422370360325a0afc))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.6.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.10...@ohif/core@2.6.11) (2020-04-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.6.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.9...@ohif/core@2.6.10) (2020-04-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.6.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.8...@ohif/core@2.6.9) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/core
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "2.6.8",
|
"version": "2.7.0",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// Commenting this out for now since it looks like Rollup is pulling in the
|
|
||||||
// Node.js version instead of the Browser version of this package
|
|
||||||
//import { btoa } from 'isomorphic-base64';
|
|
||||||
import user from '../user';
|
import user from '../user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +15,7 @@ export default function getAuthorizationHeader({ requestOptions } = {}) {
|
|||||||
// Check for OHIF.user since this can also be run on the server
|
// Check for OHIF.user since this can also be run on the server
|
||||||
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
||||||
|
|
||||||
|
// Auth for a specific server
|
||||||
if (requestOptions && requestOptions.auth) {
|
if (requestOptions && requestOptions.auth) {
|
||||||
if (typeof requestOptions.auth === 'function') {
|
if (typeof requestOptions.auth === 'function') {
|
||||||
// Custom Auth Header
|
// Custom Auth Header
|
||||||
@@ -26,7 +24,9 @@ export default function getAuthorizationHeader({ requestOptions } = {}) {
|
|||||||
// HTTP Basic Auth (user:password)
|
// HTTP Basic Auth (user:password)
|
||||||
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
||||||
}
|
}
|
||||||
} else if (accessToken) {
|
}
|
||||||
|
// Auth for the user's default
|
||||||
|
else if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import {
|
import {
|
||||||
clearStudyLoadingProgress,
|
clearStudyLoadingProgress,
|
||||||
setStudyLoadingProgress,
|
setStudyLoadingProgress,
|
||||||
} from '../redux/actions';
|
} from '../redux/actions';
|
||||||
import StackManager from '../utils/StackManager';
|
import StackManager from '../utils/StackManager';
|
||||||
import { getCornerstoneWADOImageLoader } from '../utils/cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
class BaseLoadingListener {
|
class BaseLoadingListener {
|
||||||
constructor(stack, options = {}) {
|
constructor(stack, options = {}) {
|
||||||
@@ -103,8 +103,7 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
|||||||
this._checkCachedData();
|
this._checkCachedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _checkCachedData() {
|
_checkCachedData() {
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(
|
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(
|
||||||
this._dataSetUrl
|
this._dataSetUrl
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import DICOMWeb from '../../../DICOMWeb';
|
import DICOMWeb from '../../../DICOMWeb';
|
||||||
import metadataProvider from '../../../classes/MetadataProvider';
|
import metadataProvider from '../../../classes/MetadataProvider';
|
||||||
import getWADORSImageId from '../../../utils/getWADORSImageId';
|
import getWADORSImageId from '../../../utils/getWADORSImageId';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import getReferencedSeriesSequence from './getReferencedSeriesSequence';
|
import getReferencedSeriesSequence from './getReferencedSeriesSequence';
|
||||||
import { getCornerstoneWADOImageLoader } from '../../../utils/cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a plain JS object that describes a study (a study descriptor object)
|
* Create a plain JS object that describes a study (a study descriptor object)
|
||||||
@@ -144,7 +144,7 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
wadorsuri,
|
wadorsuri,
|
||||||
wadoRoot: server.wadoRoot,
|
wadoRoot: server.wadoRoot,
|
||||||
imageRendering: server.imageRendering,
|
imageRendering: server.imageRendering,
|
||||||
thumbnailRendering: server.thumbnailRendering,
|
thumbnailRendering: server.thumbnailRendering
|
||||||
};
|
};
|
||||||
|
|
||||||
series.instances.push(sopInstance);
|
series.instances.push(sopInstance);
|
||||||
@@ -160,7 +160,6 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
|
|
||||||
const wadoRSMetadata = Object.assign(instance);
|
const wadoRSMetadata = Object.assign(instance);
|
||||||
|
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
if (sopInstance.NumberOfFrames) {
|
if (sopInstance.NumberOfFrames) {
|
||||||
for (let i = 0; i < sopInstance.NumberOfFrames; i++) {
|
for (let i = 0; i < sopInstance.NumberOfFrames; i++) {
|
||||||
const wadorsImageId = getWADORSImageId(sopInstance, i);
|
const wadorsImageId = getWADORSImageId(sopInstance, i);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export function getCornerstoneWADOImageLoader() {
|
|
||||||
return import(
|
|
||||||
/* webpackChunkName: "CornerstoneWADOImageLoader" */ 'cornerstone-wado-image-loader'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../DICOMWeb';
|
import DICOMWeb from '../DICOMWeb';
|
||||||
import { getCornerstoneWADOImageLoader } from './cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
const getImageId = imageObj => {
|
const getImageId = imageObj => {
|
||||||
if (!imageObj) {
|
if (!imageObj) {
|
||||||
@@ -86,7 +86,7 @@ const getImageLoaderType = imageId => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class DicomLoaderService {
|
class DicomLoaderService {
|
||||||
async getLocalData(dataset, studies) {
|
getLocalData(dataset, studies) {
|
||||||
if (dataset && dataset.localFile) {
|
if (dataset && dataset.localFile) {
|
||||||
// Use referenced imageInstance
|
// Use referenced imageInstance
|
||||||
const imageInstance = getImageInstance(dataset);
|
const imageInstance = getImageInstance(dataset);
|
||||||
@@ -98,7 +98,6 @@ class DicomLoaderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!someInvalidStrings(imageId)) {
|
if (!someInvalidStrings(imageId)) {
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,16 +176,16 @@ class DicomLoaderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async *getLoaderIterator(dataset, studies) {
|
*getLoaderIterator(dataset, studies) {
|
||||||
yield await this.getLocalData(dataset, studies);
|
yield this.getLocalData(dataset, studies);
|
||||||
yield this.getDataByImageType(dataset);
|
yield this.getDataByImageType(dataset);
|
||||||
yield this.getDataByDatasetType(dataset);
|
yield this.getDataByDatasetType(dataset);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findDicomDataPromise(dataset, studies) {
|
findDicomDataPromise(dataset, studies) {
|
||||||
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
||||||
// it returns first valid retriever method.
|
// it returns first valid retriever method.
|
||||||
for await (const loader of loaderIterator) {
|
for (const loader of loaderIterator) {
|
||||||
if (loader) {
|
if (loader) {
|
||||||
return loader;
|
return loader;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default function isDicomUid(subject) {
|
||||||
|
const regex = /^\d+(?:\.\d+)*$/;
|
||||||
|
return typeof subject === 'string' && regex.test(subject.trim());
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -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...'));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,19 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.2...@ohif/ui@1.4.3) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/ui
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "1.4.2",
|
"version": "1.4.3",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -9,14 +9,21 @@ export class TableList extends Component {
|
|||||||
defaultItems: PropTypes.object,
|
defaultItems: PropTypes.object,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
headerTitle: PropTypes.string,
|
headerTitle: PropTypes.string,
|
||||||
|
headless: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
headless: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="tableList">
|
<div className="tableList">
|
||||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
{!this.props.headless && (
|
||||||
{this.getHeader()}
|
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||||
</div>
|
{this.getHeader()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="tableListContent">{this.props.children}</div>
|
<div className="tableListContent">{this.props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,8 +85,12 @@ import trash from './icons/trash.svg';
|
|||||||
import unlink from './icons/unlink.svg';
|
import unlink from './icons/unlink.svg';
|
||||||
import user from './icons/user.svg';
|
import user from './icons/user.svg';
|
||||||
import youtube from './icons/youtube.svg';
|
import youtube from './icons/youtube.svg';
|
||||||
|
import eye from './icons/eye.svg';
|
||||||
|
import eyeClosed from './icons/eye-closed.svg';
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
|
eye,
|
||||||
|
'eye-closed': eyeClosed,
|
||||||
brush,
|
brush,
|
||||||
scissors,
|
scissors,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 974 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
@@ -3,6 +3,49 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [3.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.21...@ohif/viewer@3.9.0) (2020-04-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* set the authorization header for DICOMWeb requests if provided in query string ([#1646](https://github.com/OHIF/Viewers/issues/1646)) ([450c80b](https://github.com/OHIF/Viewers/commit/450c80b9d5f172be8b5713b422370360325a0afc))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.21](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.20...@ohif/viewer@3.8.21) (2020-04-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.20](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.19...@ohif/viewer@3.8.20) (2020-04-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Revert "refactor: Reduce bundle size ([#1575](https://github.com/OHIF/Viewers/issues/1575))" ([#1622](https://github.com/OHIF/Viewers/issues/1622)) ([d21af3f](https://github.com/OHIF/Viewers/commit/d21af3f133492fa31492413b8782936c9ff18b44))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.18...@ohif/viewer@3.8.19) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07)
|
## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/viewer
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "3.8.18",
|
"version": "3.9.0",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -48,16 +48,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^2.6.8",
|
"@ohif/core": "^2.7.0",
|
||||||
"@ohif/extension-cornerstone": "^2.5.1",
|
"@ohif/extension-cornerstone": "^2.5.2",
|
||||||
"@ohif/extension-dicom-html": "^1.1.2",
|
"@ohif/extension-dicom-html": "^1.1.2",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.50.9",
|
"@ohif/extension-dicom-microscopy": "^0.50.9",
|
||||||
|
"@ohif/extension-dicom-p10-downloader": "^0.0.2",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.3",
|
"@ohif/extension-dicom-pdf": "^1.0.3",
|
||||||
"@ohif/extension-dicom-segmentation": "^0.1.1",
|
"@ohif/extension-dicom-rt": "^0.0.2",
|
||||||
|
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
||||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
"@ohif/extension-vtk": "^1.3.9",
|
"@ohif/extension-vtk": "^1.3.13",
|
||||||
"@ohif/i18n": "^0.52.8",
|
"@ohif/i18n": "^0.52.8",
|
||||||
"@ohif/ui": "^1.4.2",
|
"@ohif/ui": "^1.4.3",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ import i18n from '@ohif/i18n';
|
|||||||
import './config';
|
import './config';
|
||||||
|
|
||||||
/** Utils */
|
/** Utils */
|
||||||
import { getUserManagerForOpenIdConnectClient } from './utils/index.js';
|
import {
|
||||||
|
getUserManagerForOpenIdConnectClient,
|
||||||
|
initWebWorkers,
|
||||||
|
} from './utils/index.js';
|
||||||
|
|
||||||
/** Extensions */
|
/** Extensions */
|
||||||
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
|
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
|
||||||
@@ -137,6 +140,7 @@ class App extends Component {
|
|||||||
*/
|
*/
|
||||||
_initHotkeys(appConfigHotkeys);
|
_initHotkeys(appConfigHotkeys);
|
||||||
_initServers(servers);
|
_initServers(servers);
|
||||||
|
initWebWorkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
|
import dicomParser from 'dicom-parser';
|
||||||
import version from './version.js';
|
import version from './version.js';
|
||||||
|
|
||||||
let homepage;
|
let homepage;
|
||||||
@@ -12,4 +15,31 @@ window.info = {
|
|||||||
homepage,
|
homepage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For debugging
|
||||||
|
//if (process.env.node_env === 'development') {
|
||||||
window.cornerstone = cornerstone;
|
window.cornerstone = cornerstone;
|
||||||
|
window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader;
|
||||||
|
//}
|
||||||
|
|
||||||
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||||
|
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||||
|
|
||||||
|
OHIF.user.getAccessToken = () => {
|
||||||
|
// TODO: Get the Redux store from somewhere else
|
||||||
|
const state = window.store.getState();
|
||||||
|
if (!state.oidc || !state.oidc.user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.oidc.user.access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
cornerstoneWADOImageLoader.configure({
|
||||||
|
beforeSend: function(xhr) {
|
||||||
|
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
||||||
|
|
||||||
|
if (headers.Authorization) {
|
||||||
|
xhr.setRequestHeader('Authorization', headers.Authorization);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hook that builds on useLocation to parse
|
||||||
|
* the query string for you.
|
||||||
|
*
|
||||||
|
* @name useQuery
|
||||||
|
*/
|
||||||
|
export default function() {
|
||||||
|
return new URLSearchParams(useLocation().search);
|
||||||
|
}
|
||||||
@@ -26,8 +26,10 @@ import ReactDOM from 'react-dom';
|
|||||||
import OHIFVTKExtension from '@ohif/extension-vtk';
|
import OHIFVTKExtension from '@ohif/extension-vtk';
|
||||||
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
||||||
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
||||||
|
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
|
||||||
@@ -46,6 +48,8 @@ const appProps = {
|
|||||||
OHIFDicomMicroscopyExtension,
|
OHIFDicomMicroscopyExtension,
|
||||||
OHIFDicomPDFExtension,
|
OHIFDicomPDFExtension,
|
||||||
OHIFDicomSegmentationExtension,
|
OHIFDicomSegmentationExtension,
|
||||||
|
OHIFDicomRtExtension,
|
||||||
|
OHIFDicomP10DownloaderExtension,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import FileLoaderService from './localFileLoaders/fileLoaderService';
|
|||||||
const processFile = async file => {
|
const processFile = async file => {
|
||||||
try {
|
try {
|
||||||
const fileLoaderService = new FileLoaderService(file);
|
const fileLoaderService = new FileLoaderService(file);
|
||||||
const imageId = await fileLoaderService.addFile(file);
|
const imageId = fileLoaderService.addFile(file);
|
||||||
const image = await fileLoaderService.loadFile(file, imageId);
|
const image = await fileLoaderService.loadFile(file, imageId);
|
||||||
const dataset = await fileLoaderService.getDataset(image, imageId);
|
const dataset = await fileLoaderService.getDataset(image, imageId);
|
||||||
const studies = await fileLoaderService.getStudies(dataset, imageId);
|
const studies = await fileLoaderService.getStudies(dataset, imageId);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import * as dcmjs from 'dcmjs';
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import FileLoader from './fileLoader';
|
import FileLoader from './fileLoader';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import { getCornerstoneWADOImageLoader } from '../../utils/cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||||
|
|
||||||
const DICOMFileLoader = new (class extends FileLoader {
|
const DICOMFileLoader = new (class extends FileLoader {
|
||||||
fileType = 'application/dicom';
|
fileType = 'application/dicom';
|
||||||
async loadFile(file, imageId) {
|
loadFile(file, imageId) {
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import FileLoader from './fileLoader';
|
import FileLoader from './fileLoader';
|
||||||
import PDFFileLoader from './pdfFileLoader';
|
import PDFFileLoader from './pdfFileLoader';
|
||||||
import DICOMFileLoader from './dicomFileLoader';
|
import DICOMFileLoader from './dicomFileLoader';
|
||||||
import { getCornerstoneWADOImageLoader } from '../../utils/cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
class FileLoaderService extends FileLoader {
|
class FileLoaderService extends FileLoader {
|
||||||
fileType;
|
fileType;
|
||||||
@@ -53,8 +53,7 @@ class FileLoaderService extends FileLoader {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFile(file) {
|
addFile(file) {
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
return cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
|
return cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import FileLoader from './fileLoader';
|
import FileLoader from './fileLoader';
|
||||||
import { getCornerstoneWADOImageLoader } from '../../utils/cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
const PDFFileLoader = new (class extends FileLoader {
|
const PDFFileLoader = new (class extends FileLoader {
|
||||||
fileType = 'application/pdf';
|
fileType = 'application/pdf';
|
||||||
async loadFile(file, imageId) {
|
loadFile(file, imageId) {
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { utils, user } from '@ohif/core';
|
||||||
|
//
|
||||||
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData';
|
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData';
|
||||||
import useServer from '../customHooks/useServer';
|
import useServer from '../customHooks/useServer';
|
||||||
import OHIF from '@ohif/core';
|
import useQuery from '../customHooks/useQuery';
|
||||||
const { urlUtil: UrlUtil } = OHIF.utils;
|
const { urlUtil: UrlUtil } = utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get array of seriesUIDs from param or from queryString
|
* Get array of seriesUIDs from param or from queryString
|
||||||
@@ -27,8 +29,19 @@ function ViewerRouting({ match: routeMatch, location: routeLocation }) {
|
|||||||
studyInstanceUIDs,
|
studyInstanceUIDs,
|
||||||
seriesInstanceUIDs,
|
seriesInstanceUIDs,
|
||||||
} = routeMatch.params;
|
} = routeMatch.params;
|
||||||
const server = useServer({ project, location, dataset, dicomStore });
|
|
||||||
|
|
||||||
|
// Set the user's default authToken for outbound DICOMWeb requests.
|
||||||
|
// Is only applied if target server does not set `requestOptions` property.
|
||||||
|
//
|
||||||
|
// See: `getAuthorizationHeaders.js`
|
||||||
|
let query = useQuery();
|
||||||
|
const authToken = query.get('token');
|
||||||
|
|
||||||
|
if (authToken) {
|
||||||
|
user.getAccessToken = () => authToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = useServer({ project, location, dataset, dicomStore });
|
||||||
const studyUIDs = UrlUtil.paramString.parseParam(studyInstanceUIDs);
|
const studyUIDs = UrlUtil.paramString.parseParam(studyInstanceUIDs);
|
||||||
const seriesUIDs = getSeriesInstanceUIDs(seriesInstanceUIDs, routeLocation);
|
const seriesUIDs = getSeriesInstanceUIDs(seriesInstanceUIDs, routeLocation);
|
||||||
|
|
||||||
@@ -55,6 +68,7 @@ ViewerRouting.propTypes = {
|
|||||||
project: PropTypes.string,
|
project: PropTypes.string,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
location: PropTypes.any,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewerRouting;
|
export default ViewerRouting;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import asyncComponent from '../components/AsyncComponent.js';
|
import asyncComponent from '../components/AsyncComponent.js';
|
||||||
import { withCornerstone } from '../utils/cornerstoneWADOImageLoader';
|
|
||||||
|
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
const { urlUtil: UrlUtil } = OHIF.utils;
|
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||||
@@ -10,10 +9,8 @@ const IHEInvokeImageDisplay = asyncComponent(() =>
|
|||||||
/* webpackChunkName: "IHEInvokeImageDisplay" */ './IHEInvokeImageDisplay.js'
|
/* webpackChunkName: "IHEInvokeImageDisplay" */ './IHEInvokeImageDisplay.js'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const ViewerRouting = withCornerstone(
|
const ViewerRouting = asyncComponent(() =>
|
||||||
asyncComponent(() =>
|
import(/* webpackChunkName: "ViewerRouting" */ './ViewerRouting.js')
|
||||||
import(/* webpackChunkName: "ViewerRouting" */ './ViewerRouting.js')
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const StudyListRouting = asyncComponent(() =>
|
const StudyListRouting = asyncComponent(() =>
|
||||||
@@ -21,18 +18,14 @@ const StudyListRouting = asyncComponent(() =>
|
|||||||
/* webpackChunkName: "StudyListRouting" */ '../studylist/StudyListRouting.js'
|
/* webpackChunkName: "StudyListRouting" */ '../studylist/StudyListRouting.js'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const StandaloneRouting = withCornerstone(
|
const StandaloneRouting = asyncComponent(() =>
|
||||||
asyncComponent(() =>
|
import(
|
||||||
import(
|
/* webpackChunkName: "ConnectedStandaloneRouting" */ '../connectedComponents/ConnectedStandaloneRouting.js'
|
||||||
/* webpackChunkName: "ConnectedStandaloneRouting" */ '../connectedComponents/ConnectedStandaloneRouting.js'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const ViewerLocalFileData = withCornerstone(
|
const ViewerLocalFileData = asyncComponent(() =>
|
||||||
asyncComponent(() =>
|
import(
|
||||||
import(
|
/* webpackChunkName: "ViewerLocalFileData" */ '../connectedComponents/ViewerLocalFileData.js'
|
||||||
/* webpackChunkName: "ViewerLocalFileData" */ '../connectedComponents/ViewerLocalFileData.js'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import OHIF from '@ohif/core';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import dicomParser from 'dicom-parser';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export function getCornerstoneWADOImageLoader() {
|
|
||||||
return import(
|
|
||||||
/* webpackChunkName: "CornerstoneWADOImageLoader" */ 'cornerstone-wado-image-loader'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
export async function initCornerstoneWADOImageLoader() {
|
|
||||||
if (initialized) return;
|
|
||||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
|
||||||
// For debugging
|
|
||||||
//if (process.env.node_env === 'development') {
|
|
||||||
window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader;
|
|
||||||
//}
|
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
|
||||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
|
||||||
|
|
||||||
OHIF.user.getAccessToken = () => {
|
|
||||||
// TODO: Get the Redux store from somewhere else
|
|
||||||
const state = window.store.getState();
|
|
||||||
if (!state.oidc || !state.oidc.user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.oidc.user.accesstoken;
|
|
||||||
};
|
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.configure({
|
|
||||||
beforeSend: function (xhr) {
|
|
||||||
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
|
||||||
|
|
||||||
if (headers.Authorization) {
|
|
||||||
xhr.setRequestHeader('Authorization', headers.Authorization);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
|
|
||||||
startWebWorkersOnDemand: true,
|
|
||||||
taskConfiguration: {
|
|
||||||
decodeTask: {
|
|
||||||
initializeCodecsOnStartup: false,
|
|
||||||
usePDFJS: false,
|
|
||||||
strict: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function withCornerstone(Component) {
|
|
||||||
// eslint-disable-next-line react/display-name
|
|
||||||
return props => {
|
|
||||||
const [
|
|
||||||
cornerstoneWADOImageLoaderInitialized,
|
|
||||||
setCornerstoneWADOImageLoaderInitialized,
|
|
||||||
] = React.useState(false);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
initCornerstoneWADOImageLoader().then(() =>
|
|
||||||
setCornerstoneWADOImageLoaderInitialized(true)
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!cornerstoneWADOImageLoaderInitialized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <Component {...props} />;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
|
import getUserManagerForOpenIdConnectClient from './getUserManagerForOpenIdConnectClient.js';
|
||||||
|
import initWebWorkers from './initWebWorkers.js';
|
||||||
|
|
||||||
export { getUserManagerForOpenIdConnectClient };
|
export { getUserManagerForOpenIdConnectClient, initWebWorkers };
|
||||||
@@ -5,7 +5,7 @@ describe('utils', () => {
|
|||||||
const utilExports = Object.keys(utils).sort();
|
const utilExports = Object.keys(utils).sort();
|
||||||
|
|
||||||
expect(utilExports).toEqual(
|
expect(utilExports).toEqual(
|
||||||
['getUserManagerForOpenIdConnectClient'].sort()
|
['getUserManagerForOpenIdConnectClient', 'initWebWorkers'].sort()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export default function initWebWorkers() {
|
||||||
|
const config = {
|
||||||
|
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||||
|
startWebWorkersOnDemand: true,
|
||||||
|
taskConfiguration: {
|
||||||
|
decodeTask: {
|
||||||
|
initializeCodecsOnStartup: false,
|
||||||
|
usePDFJS: false,
|
||||||
|
strict: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import initWebWorkers from './initWebWorkers.js';
|
||||||
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
|
|
||||||
|
describe('initWebWorkers', () => {
|
||||||
|
it("initializes cornerstoneWADOImageLoader's web workers", () => {
|
||||||
|
initWebWorkers();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
cornerstoneWADOImageLoader.webWorkerManager.initialize
|
||||||
|
).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initWebWorkers', () => {
|
||||||
|
it("initializes cornerstoneWADOImageLoader's web workers only once", () => {
|
||||||
|
initWebWorkers();
|
||||||
|
initWebWorkers();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
cornerstoneWADOImageLoader.webWorkerManager.initialize
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user