feat: cornerstone3D stack and volume viewports (#2787)
* feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: cs3d tools and toolGroups (#20) * add more tools to work with cs3d mode * fix hotkeys * add stack manager usage for stach viewports * add image scrollbar * wip viewport overlay * fix toAnnotation schema for tools * fix the unnecessary size change that triggered resize * hanging protocol improvement to allow unmatched errors * study description matching for hanging protocol * fix the displaysetOptions to work * fix handle the active tool when a new viewport is added * fix separate toolGroups for mode * apply review comments * apply review comments * yarn lock * feat: overlay component (#21) * fix default displayset options * add viewport overlay * apply review comments * feat: loading and orientation indicators (#22) * add loading indicator * add orientation marker initial work * apply review comments * fix: orientation markers (#23) * finished the orientation markers * fix various broken cypress tests * apply review comments * update yarn lock * feat: re-working measurement tracking mode with cornerstone3d (#2805) * feat: cs3d working stack viewport and tools (#19) * Squashed everything * fix weird react issue * fix eslint / prettier stuff * thumbnails work now with cpu rendering * feat: use new loadImageToCanvas in cs3d * make jump to measurement work * fix active thumbnail * fix measurement delete * fix window level presets * remove segmentation and sync groups for now * fix the dicom pdf and dicom video * fix cornerstone window assignment for cypress * apply review comments Co-authored-by: Erik <erik.sweed@gmail.com> * feat: Add Measurement tracking mode with cs3D (#2789) * feat: first render for cornerstone3d tracked viewport * make tool active work * wip for SR extension * renamed dicom sr to cornerstone dicom sr * remove cornerstone from dicom pdf and video * move dicom sr logic to sr extension * feat: Add hydration for the length tool * fix SR display tool for length using cs3d * fix default config * fix: various bugs with sr viewport and tracking * fix promptying to continue tracking for when SR is created * feat: add keep trackign of unique identifiers * fix hydration for same imageIds * feat: Add SR toolGroup creation on modeEnter * feat: remove the need for separate mapper for SR hydration * add SR display for ellipse * handle hydration of elliptical ROI tool * remove cornerstone extension * add arrow mapping * feat: Add ArrowAnnotate SR display and hydration * apply review comments * apply review comments * move viewport labels to the viewportData * apply review comments * fix: integration cypress tests with cornerstone3D and add CINE tool (#2795) * fix: integration cypress tests with cornerstone3D * revert to addOrUpdate as it makes more sense * fix local drag and drop * fix tests * move dicomLoaderService to cornerstone extension * fix various import bugs * fix bug for local PT series * fix various unit tests * bump cs3d versions * add angle and magnify tool * bump dependencies to avoid broken peerDeps * feat: add initial work for capture using cs3d * feat: show annotations on the image capture * feat: add svg layer export * feat: Add CINE Tool * feat: remove unnecessary viewport rendering for cine state changes Co-authored-by: Erik Ziegler <erik.sweed@gmail.com> * docs: modify and improve documentation (#2800) * cleanup docs versionings * feat: Add all version explanations * version docs for 3.0 * wip for changing docs * wip for updated docs * add utility module documentation * fix demo with nohoisting of history * add slides and video to resources * apply review comments * fix: drag and drop SR into SR viewport (#2803) * update yarn lock Co-authored-by: Erik <erik.sweed@gmail.com> * update pathnames to match v3-stable * update the e2e pathname * fix: various bugs with regard to tracking workflow (#2811) * fix: various issues with measurement panel * fix: update default tool style for annotations * fix: annotatoin label getting removed * feat: Add backward compatibility for SR hydration with legacy cornerstone * fix: cursors and ellipse ROI max style * fix: ArrowAnnotate SRDisplay * apply review comments * bump package versions * fix: bugin rehydration of SR * fix: e2e tests * fix active viewport thickness and arrowTool ui * add readme for measurement tracking * use uploaded image for readme * add back images * try to fix e2e test * fix: window level presets hotkeys * Update README.md * update yarn lock * feat: volume api and TMTV mode (#2817) * feat: volumeAPI and TMTV mode * feat: use cs3d cache service to obtain viewportData * wip for volume api * wip: fusion viewport * feat: add blend mode option * wip for image scrollbar * fix drag and drop thumbnail * wip for image scrollbar for voluems * fix: element mismatch bug for scroll * feat: Add image scrollbar to volumes * feat: add syncGroups to volume api * feat: add tmtv mode initial setup * feat: add initial image options for the stack viewports * feat: Add custom load strategy for volume viewports via HP * apply review comments fix: Jump presets cs3d (#2812) * feat: Add JumpPreset to OHIF for Cornerstone3D * fix: accessing viewport service from servicesManager feat: volume API and TMTV mode (#2814) * fix: do not display overlays on mip viewports * feat: add optional disableCommands for toggle buttons * fix: toggleCrossharis * feat: config the crosshairs * feat: add PetSUV Panel for changing metadata * feat: initial work for the rectangleROIThreshold panel * wip: measurement service * roi threshold working * feat: Add displayText to segmentations * feat: add remove segmentation * feat: add csv export * feat: add RT export for annotations in tmtv mode * fix: fusion to use pt in tmtv mode and measuremet mappings * fix: various bugs * apply review comments * apply review comments * feat: add fusion color maps * add readme to tmtv mode * Update README.md * fix: try to fix build * fix unit tests * Update README.md * feat: add about to the panel * fix: changing strategy in roi panel * apply review comments * update package versions * wip for stackPrefetch * fix: cornerstone3d hydration and renaming (#2818) * update readme * renamed cornerstone extension * wip for renaming cornerstone3D variables * wip for renaming cornerstone3D variables * wip for fixing bugs for SR viewport * fix: jumpToMeasurement and initial label after hydration * fix: fileName capitalization * feat: use the new prefetch stack in the cs3d (#2820) * feat: use the new prefetch stack in the cs3d * use viewport scroll api for stack viewport * fix cine stop when scrollbar changes * feat: use new prefetch events * fix loading state to not show repeatedly * fix: various bugs for tmtv mode thresholding and new icons (#2823) * feat: make tmtv mode available in worklist * feat: add new icons for tmtv mode * feat: add fusion color icon * fix: parallel scale calculation * fix: bump Cornerstone to get large image support working * fix: Fix issues with CPU viewport flipping, including config files * fix: bump cornerstone to fix magnify tool * fix: Bump Cornerstone version to fix resetCamera issue in StackViewport * ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys * fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors * feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838) Also works with the compile time add that the existing cli uses, so that both build types work. Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes. * bump: dependency versions to fix hydration bugs (#2848) * bump: dcmjs version to fix hydration bugs * try to fix tests * bump dependency versions Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
parent
86dcaf6293
commit
bd1c38c00c
@ -1,11 +1,18 @@
|
|||||||
{
|
{
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"import",
|
||||||
|
"eslint-plugin-tsdoc",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
],
|
],
|
||||||
"parser": "babel-eslint",
|
"parser": "@typescript-eslint/parser",
|
||||||
"env": {
|
"env": {
|
||||||
"jest": true
|
"jest": true
|
||||||
},
|
},
|
||||||
|
|||||||
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@ -11,5 +11,18 @@
|
|||||||
"url": "http://localhost:3000",
|
"url": "http://localhost:3000",
|
||||||
"webRoot": "${workspaceFolder}"
|
"webRoot": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// "name": "Debug Jest Tests",
|
||||||
|
// "type": "node",
|
||||||
|
// "request": "launch",
|
||||||
|
// "runtimeArgs": [
|
||||||
|
// "--inspect-brk",
|
||||||
|
// "${workspaceRoot}/node_modules/.bin/jest",
|
||||||
|
// "--runInBand"
|
||||||
|
// ],
|
||||||
|
// "console": "integratedTerminal",
|
||||||
|
// "internalConsoleOptions": "neverOpen",
|
||||||
|
// "port": 9229
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ function transpileJavaScript(mode) {
|
|||||||
const exclude =
|
const exclude =
|
||||||
mode === 'production'
|
mode === 'production'
|
||||||
? excludeNodeModulesExcept([
|
? excludeNodeModulesExcept([
|
||||||
'vtk.js',
|
|
||||||
// 'dicomweb-client',
|
// 'dicomweb-client',
|
||||||
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
|
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
|
||||||
'react-dnd',
|
'react-dnd',
|
||||||
@ -21,7 +20,8 @@ function transpileJavaScript(mode) {
|
|||||||
: excludeNodeModulesExcept([]);
|
: excludeNodeModulesExcept([]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test: /\.(mjs|js|jsx)?$/,
|
// Include mjs, ts, tsx, js, and jsx files.
|
||||||
|
test: /\.(mjs|ts|js)x?$/,
|
||||||
// These are packages that are not transpiled to our lowest supported
|
// These are packages that are not transpiled to our lowest supported
|
||||||
// JS version (currently ES5). Most of these leverage ES6+ features,
|
// JS version (currently ES5). Most of these leverage ES6+ features,
|
||||||
// that we need to transpile to a different syntax.
|
// that we need to transpile to a different syntax.
|
||||||
|
|||||||
@ -12,6 +12,8 @@ const CopyPlugin = require('copy-webpack-plugin');
|
|||||||
|
|
||||||
// ~~ PackageJSON
|
// ~~ PackageJSON
|
||||||
const PACKAGE = require('../platform/viewer/package.json');
|
const PACKAGE = require('../platform/viewer/package.json');
|
||||||
|
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
|
||||||
|
// .rules;
|
||||||
// ~~ RULES
|
// ~~ RULES
|
||||||
const loadShadersRule = require('./rules/loadShaders.js');
|
const loadShadersRule = require('./rules/loadShaders.js');
|
||||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||||
@ -66,11 +68,20 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
children: false,
|
children: false,
|
||||||
warnings: true,
|
warnings: true,
|
||||||
},
|
},
|
||||||
|
devServer: {
|
||||||
|
open: true,
|
||||||
|
port: 3000,
|
||||||
|
historyApiFallback: true,
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
},
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
transpileJavaScriptRule(mode),
|
transpileJavaScriptRule(mode),
|
||||||
loadWebWorkersRule,
|
loadWebWorkersRule,
|
||||||
loadShadersRule,
|
// loadShadersRule,
|
||||||
{
|
{
|
||||||
test: /\.m?js/,
|
test: /\.m?js/,
|
||||||
resolve: {
|
resolve: {
|
||||||
@ -78,7 +89,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
cssToJavaScript,
|
cssToJavaScript,
|
||||||
],
|
], //.concat(vtkRules),
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
mainFields: ['module', 'browser', 'main'],
|
mainFields: ['module', 'browser', 'main'],
|
||||||
@ -106,7 +117,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
|||||||
SRC_DIR,
|
SRC_DIR,
|
||||||
],
|
],
|
||||||
// Attempt to resolve these extensions in order.
|
// Attempt to resolve these extensions in order.
|
||||||
extensions: ['.js', '.jsx', '.json', '*'],
|
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
|
||||||
// symlinked resources are resolved to their real path, not their symlinked location
|
// symlinked resources are resolved to their real path, not their symlinked location
|
||||||
symlinks: true,
|
symlinks: true,
|
||||||
fallback: { fs: false, path: false, zlib: false },
|
fallback: { fs: false, path: false, zlib: false },
|
||||||
|
|||||||
@ -190,13 +190,14 @@ you'll see the following:
|
|||||||
├── extensions #
|
├── extensions #
|
||||||
│ ├── _example # Skeleton of example extension
|
│ ├── _example # Skeleton of example extension
|
||||||
│ ├── default #
|
│ ├── default #
|
||||||
│ ├── cornerstone # 2D images w/ Cornerstone.js
|
│ ├── cornerstone # image rendering and tools w/ Cornerstone
|
||||||
│ ├── dicom-sr #
|
│ ├── cornerstone- dicom-sr #
|
||||||
│ └── measurement-tracking #
|
│ └── measurement-tracking #
|
||||||
│
|
│
|
||||||
├── modes #
|
├── modes #
|
||||||
│ ├── _example # Skeleton of example mode
|
│ ├── _example # Skeleton of example mode
|
||||||
│ └── longitudinal #
|
│ ├── basic-dev-mode # Basic development mode
|
||||||
|
│ └── longitudinal # Longitudinal mode (measurement tracking)
|
||||||
│
|
│
|
||||||
├── platform #
|
├── platform #
|
||||||
│ ├── core # Business Logic
|
│ ├── core # Business Logic
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
const aliases = require('./aliases.config');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// https://babeljs.io/docs/en/options#babelrcroots
|
// https://babeljs.io/docs/en/options#babelrcroots
|
||||||
module.exports = {
|
module.exports = {
|
||||||
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
||||||
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
|
presets: [
|
||||||
|
'@babel/preset-env',
|
||||||
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'inline-react-svg',
|
||||||
|
'@babel/plugin-proposal-class-properties',
|
||||||
|
'@babel/plugin-transform-typescript',
|
||||||
|
],
|
||||||
env: {
|
env: {
|
||||||
test: {
|
test: {
|
||||||
presets: [
|
presets: [
|
||||||
@ -17,12 +23,14 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'@babel/preset-react',
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-typescript',
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/plugin-proposal-object-rest-spread',
|
'@babel/plugin-proposal-object-rest-spread',
|
||||||
'@babel/plugin-syntax-dynamic-import',
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
'@babel/plugin-transform-regenerator',
|
'@babel/plugin-transform-regenerator',
|
||||||
'@babel/plugin-transform-runtime',
|
'@babel/plugin-transform-runtime',
|
||||||
|
'@babel/plugin-transform-typescript',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
@ -30,6 +38,7 @@ module.exports = {
|
|||||||
// WebPack handles ES6 --> Target Syntax
|
// WebPack handles ES6 --> Target Syntax
|
||||||
['@babel/preset-env', { modules: false }],
|
['@babel/preset-env', { modules: false }],
|
||||||
'@babel/preset-react',
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-typescript',
|
||||||
],
|
],
|
||||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||||
},
|
},
|
||||||
@ -38,6 +47,7 @@ module.exports = {
|
|||||||
// WebPack handles ES6 --> Target Syntax
|
// WebPack handles ES6 --> Target Syntax
|
||||||
['@babel/preset-env', { modules: false }],
|
['@babel/preset-env', { modules: false }],
|
||||||
'@babel/preset-react',
|
'@babel/preset-react',
|
||||||
|
'@babel/preset-typescript',
|
||||||
],
|
],
|
||||||
plugins: ['react-hot-loader/babel'],
|
plugins: ['react-hot-loader/babel'],
|
||||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||||
|
|||||||
@ -14,7 +14,6 @@ module.exports = (env, argv) => {
|
|||||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
|
||||||
return merge(commonConfig, {
|
return merge(commonConfig, {
|
||||||
devtool: 'source-map',
|
|
||||||
stats: {
|
stats: {
|
||||||
colors: true,
|
colors: true,
|
||||||
hash: true,
|
hash: true,
|
||||||
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-sr",
|
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "OHIF/Viewers",
|
"repository": "OHIF/Viewers",
|
||||||
"main": "dist/index.umd.js",
|
"main": "dist/index.umd.js",
|
||||||
"module": "src/index.js",
|
"module": "src/index.tsx",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14",
|
"node": ">=14",
|
||||||
"npm": ">=6",
|
"npm": ">=6",
|
||||||
@ -34,21 +34,18 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"cornerstone-core": "^2.6.0",
|
"dcmjs": "^0.24.5",
|
||||||
"cornerstone-math": "^0.1.9",
|
|
||||||
"cornerstone-tools": "6.0.2",
|
|
||||||
"cornerstone-wado-image-loader": "4.0.4",
|
|
||||||
"dcmjs": "0.16.1",
|
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-cornerstone-viewport": "4.1.2",
|
|
||||||
"@ohif/extension-cornerstone": "^3.0.0",
|
"@ohif/extension-cornerstone": "^3.0.0",
|
||||||
"@ohif/extension-measurement-tracking": "^3.0.0"
|
"@ohif/extension-measurement-tracking": "^3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.3",
|
"@babel/runtime": "7.16.3",
|
||||||
"classnames": "^2.2.6"
|
"classnames": "^2.2.6",
|
||||||
|
"@cornerstonejs/core": "^0.13.11",
|
||||||
|
"@cornerstonejs/tools": "^0.20.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
143
extensions/cornerstone-dicom-sr/src/commandsModule.js
Normal file
143
extensions/cornerstone-dicom-sr/src/commandsModule.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { metaData, utilities } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
|
import getFilteredCornerstoneToolState from './utils/getFilteredCornerstoneToolState';
|
||||||
|
|
||||||
|
const { MeasurementReport } = dcmjs.adapters.Cornerstone3D;
|
||||||
|
const { log } = OHIF;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param measurementData An array of measurements from the measurements service
|
||||||
|
* that you wish to serialize.
|
||||||
|
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||||
|
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const _generateReport = (
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options = {}
|
||||||
|
) => {
|
||||||
|
const filteredToolState = getFilteredCornerstoneToolState(
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes
|
||||||
|
);
|
||||||
|
|
||||||
|
const report = MeasurementReport.generateReport(
|
||||||
|
filteredToolState,
|
||||||
|
metaData,
|
||||||
|
utilities.worldToImageCoords
|
||||||
|
);
|
||||||
|
|
||||||
|
const { dataset } = report;
|
||||||
|
|
||||||
|
// Add in top level series options
|
||||||
|
Object.assign(dataset, options);
|
||||||
|
|
||||||
|
return dataset;
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandsModule = ({}) => {
|
||||||
|
const actions = {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param measurementData An array of measurements from the measurements service
|
||||||
|
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||||
|
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||||
|
* as opposed to Finding Sites.
|
||||||
|
* that you wish to serialize.
|
||||||
|
*/
|
||||||
|
downloadReport: ({
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options = {},
|
||||||
|
}) => {
|
||||||
|
const srDataset = actions.generateReport(
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||||
|
|
||||||
|
//Create a URL for the binary.
|
||||||
|
var objectUrl = URL.createObjectURL(reportBlob);
|
||||||
|
window.location.assign(objectUrl);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param measurementData An array of measurements from the measurements service
|
||||||
|
* that you wish to serialize.
|
||||||
|
* @param dataSource The dataSource that you wish to use to persist the data.
|
||||||
|
* @param additionalFindingTypes toolTypes that should be stored with labels as Findings
|
||||||
|
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||||
|
* @return The naturalized report
|
||||||
|
*/
|
||||||
|
storeMeasurements: async ({
|
||||||
|
measurementData,
|
||||||
|
dataSource,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options = {},
|
||||||
|
}) => {
|
||||||
|
// TODO -> Eventually use the measurements directly and not the dcmjs adapter,
|
||||||
|
// But it is good enough for now whilst we only have cornerstone as a datasource.
|
||||||
|
log.info('[DICOMSR] storeMeasurements');
|
||||||
|
|
||||||
|
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
|
||||||
|
log.error(
|
||||||
|
'[DICOMSR] datasource has no dataSource.store.dicom endpoint!'
|
||||||
|
);
|
||||||
|
return Promise.reject({});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const naturalizedReport = _generateReport(
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
const { StudyInstanceUID } = naturalizedReport;
|
||||||
|
|
||||||
|
await dataSource.store.dicom(naturalizedReport);
|
||||||
|
|
||||||
|
if (StudyInstanceUID) {
|
||||||
|
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return naturalizedReport;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
log.error(
|
||||||
|
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
error.message || 'Error while saving the measurements.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = {
|
||||||
|
downloadReport: {
|
||||||
|
commandFn: actions.downloadReport,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
storeMeasurements: {
|
||||||
|
commandFn: actions.storeMeasurements,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions,
|
||||||
|
definitions,
|
||||||
|
defaultContext: 'CORNERSTONE_STRUCTURED_REPORT',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default commandsModule;
|
||||||
@ -2,9 +2,11 @@ import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
|||||||
import { utils, classes } from '@ohif/core';
|
import { utils, classes } from '@ohif/core';
|
||||||
import addMeasurement from './utils/addMeasurement';
|
import addMeasurement from './utils/addMeasurement';
|
||||||
import isRehydratable from './utils/isRehydratable';
|
import isRehydratable from './utils/isRehydratable';
|
||||||
|
import { adapters } from 'dcmjs';
|
||||||
|
|
||||||
|
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D;
|
||||||
|
|
||||||
const { ImageSet } = classes;
|
const { ImageSet } = classes;
|
||||||
|
|
||||||
// TODO ->
|
// TODO ->
|
||||||
// Add SR thumbnail
|
// Add SR thumbnail
|
||||||
// Make viewport
|
// Make viewport
|
||||||
@ -16,6 +18,9 @@ const sopClassUids = [
|
|||||||
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||||
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||||
|
|
||||||
const CodeNameCodeSequenceValues = {
|
const CodeNameCodeSequenceValues = {
|
||||||
ImagingMeasurementReport: '126000',
|
ImagingMeasurementReport: '126000',
|
||||||
ImageLibrary: '111028',
|
ImageLibrary: '111028',
|
||||||
@ -26,12 +31,15 @@ const CodeNameCodeSequenceValues = {
|
|||||||
TrackingIdentifier: '112039',
|
TrackingIdentifier: '112039',
|
||||||
Finding: '121071',
|
Finding: '121071',
|
||||||
FindingSite: 'G-C0E3', // SRT
|
FindingSite: 'G-C0E3', // SRT
|
||||||
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
|
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
|
||||||
};
|
};
|
||||||
|
|
||||||
const CodingSchemeDesignators = {
|
const CodingSchemeDesignators = {
|
||||||
SRT: 'SRT',
|
SRT: 'SRT',
|
||||||
cornerstoneTools4: 'CST4',
|
CornerstoneCodeSchemes: [
|
||||||
|
Cornerstone3DCodeScheme.CodingSchemeDesignator,
|
||||||
|
'CST4',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATIONSHIP_TYPE = {
|
const RELATIONSHIP_TYPE = {
|
||||||
@ -117,8 +125,8 @@ function _load(displaySet, servicesManager, extensionManager) {
|
|||||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||||
|
|
||||||
const mappings = MeasurementService.getSourceMappings(
|
const mappings = MeasurementService.getSourceMappings(
|
||||||
'CornerstoneTools',
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||||
'4'
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||||
);
|
);
|
||||||
|
|
||||||
displaySet.isHydrated = false;
|
displaySet.isHydrated = false;
|
||||||
@ -217,7 +225,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
|
|
||||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||||
const measurement = unloadedMeasurements[j];
|
const measurement = unloadedMeasurements[j];
|
||||||
if (_measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
|
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
|
||||||
addMeasurement(
|
addMeasurement(
|
||||||
measurement,
|
measurement,
|
||||||
imageId,
|
imageId,
|
||||||
@ -231,7 +239,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
|
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
|
||||||
const { coords } = measurement;
|
const { coords } = measurement;
|
||||||
|
|
||||||
for (let j = 0; j < coords.length; j++) {
|
for (let j = 0; j < coords.length; j++) {
|
||||||
@ -458,8 +466,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
Finding &&
|
Finding &&
|
||||||
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
Finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||||
|
) &&
|
||||||
Finding.ConceptCodeSequence.CodeValue ===
|
Finding.ConceptCodeSequence.CodeValue ===
|
||||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||||
) {
|
) {
|
||||||
@ -473,8 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
if (FindingSites.length) {
|
if (FindingSites.length) {
|
||||||
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
||||||
FindingSite =>
|
FindingSite =>
|
||||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||||
|
) &&
|
||||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||||
);
|
);
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
import onModeEnter from './onModeEnter';
|
import onModeEnter from './onModeEnter';
|
||||||
|
import commandsModule from './commandsModule';
|
||||||
import init from './init';
|
import init from './init';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
|
import toolNames from './tools/toolNames';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -21,11 +23,12 @@ const OHIFCornerstoneSRViewport = props => {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export default {
|
const dicomSRExtension = {
|
||||||
/**
|
/**
|
||||||
* Only required property. Should be a unique value across all extensions.
|
* Only required property. Should be a unique value across all extensions.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
|
onModeEnter,
|
||||||
|
|
||||||
preRegistration({ servicesManager, configuration = {} }) {
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
@ -50,50 +53,24 @@ export default {
|
|||||||
|
|
||||||
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
|
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
|
||||||
},
|
},
|
||||||
getCommandsModule({ servicesManager }) {
|
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||||
return {
|
return commandsModule({
|
||||||
definitions: {
|
servicesManager,
|
||||||
setToolActive: {
|
commandsManager,
|
||||||
commandFn: ({ toolName, element }) => {
|
extensionManager,
|
||||||
if (!toolName) {
|
});
|
||||||
console.warn('No toolname provided to setToolActive command');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set same tool or alt tool
|
|
||||||
const toolAlias = _getToolAlias(toolName);
|
|
||||||
|
|
||||||
cornerstoneTools.setToolActiveForElement(element, toolAlias, {
|
|
||||||
mouseButtonMask: 1,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::STRUCTURED_REPORT',
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
onModeEnter,
|
getUtilityModule({ servicesManager }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'tools',
|
||||||
|
exports: {
|
||||||
|
toolNames,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function _getToolAlias(toolName) {
|
export default dicomSRExtension;
|
||||||
let toolAlias = toolName;
|
|
||||||
|
|
||||||
switch (toolName) {
|
|
||||||
case 'Length':
|
|
||||||
toolAlias = 'SRLength';
|
|
||||||
break;
|
|
||||||
case 'Bidirectional':
|
|
||||||
toolAlias = 'SRBidirectional';
|
|
||||||
break;
|
|
||||||
case 'ArrowAnnotate':
|
|
||||||
toolAlias = 'SRArrowAnnotate';
|
|
||||||
break;
|
|
||||||
case 'EllipticalRoi':
|
|
||||||
toolAlias = 'SREllipticalRoi';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return toolAlias;
|
|
||||||
}
|
|
||||||
29
extensions/cornerstone-dicom-sr/src/init.js
Normal file
29
extensions/cornerstone-dicom-sr/src/init.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { addTool, annotation } from '@cornerstonejs/tools';
|
||||||
|
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||||
|
import SRLengthTool from './tools/tools/SRLength';
|
||||||
|
import SRBidirectionalTool from './tools/tools/SRBidirectional';
|
||||||
|
import SREllipticalROITool from './tools/tools/SREllipticalROI';
|
||||||
|
import SRArrowAnnotateTool from './tools/tools/SRArrowAnnotate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} configuration
|
||||||
|
*/
|
||||||
|
export default function init({ configuration = {} }) {
|
||||||
|
addTool(DICOMSRDisplayTool);
|
||||||
|
addTool(SRLengthTool);
|
||||||
|
addTool(SRBidirectionalTool);
|
||||||
|
addTool(SREllipticalROITool);
|
||||||
|
addTool(SRArrowAnnotateTool);
|
||||||
|
|
||||||
|
// Modify annotation tools to use dashed lines on SR
|
||||||
|
const dashedLine = {
|
||||||
|
lineDash: '4,4',
|
||||||
|
};
|
||||||
|
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
|
||||||
|
[SRLengthTool.toolName]: dashedLine,
|
||||||
|
[SRBidirectionalTool.toolName]: dashedLine,
|
||||||
|
[SREllipticalROITool.toolName]: dashedLine,
|
||||||
|
[SRArrowAnnotateTool.toolName]: dashedLine,
|
||||||
|
global: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
388
extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts
Normal file
388
extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
import { Types, metaData, utilities as csUtils } from '@cornerstonejs/core';
|
||||||
|
import {
|
||||||
|
AnnotationTool,
|
||||||
|
annotation,
|
||||||
|
drawing,
|
||||||
|
utilities,
|
||||||
|
Types as cs3DToolsTypes,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
|
import { getTrackingUniqueIdentifiersForElement } from './modules/dicomSRModule';
|
||||||
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
|
|
||||||
|
export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||||
|
static toolName = 'DICOMSRDisplay';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
toolProps = {},
|
||||||
|
defaultToolProps = {
|
||||||
|
configuration: {},
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
super(toolProps, defaultToolProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getTextBoxLinesFromLabels(labels) {
|
||||||
|
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||||
|
|
||||||
|
const labelLength = Math.min(labels.length, 3);
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < labelLength; i++) {
|
||||||
|
const labelEntry = labels[i];
|
||||||
|
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This tool should not inherit from AnnotationTool and we should not need
|
||||||
|
// to add the following lines.
|
||||||
|
isPointNearTool = () => null;
|
||||||
|
getHandleNearImagePoint = () => null;
|
||||||
|
|
||||||
|
renderAnnotation = (
|
||||||
|
enabledElement: Types.IEnabledElement,
|
||||||
|
svgDrawingHelper: any
|
||||||
|
): void => {
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
const { element } = viewport;
|
||||||
|
|
||||||
|
let annotations = annotation.state.getAnnotations(
|
||||||
|
element,
|
||||||
|
this.getToolName()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
|
||||||
|
if (!annotations?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
annotations = this.filterInteractableAnnotationsForElement(
|
||||||
|
element,
|
||||||
|
annotations
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!annotations?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(
|
||||||
|
element
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
activeIndex,
|
||||||
|
trackingUniqueIdentifiers,
|
||||||
|
} = trackingUniqueIdentifiersForElement;
|
||||||
|
|
||||||
|
const activeTrackingUniqueIdentifier =
|
||||||
|
trackingUniqueIdentifiers[activeIndex];
|
||||||
|
|
||||||
|
// Filter toolData to only render the data for the active SR.
|
||||||
|
const filteredAnnotations = annotations.filter(annotation =>
|
||||||
|
trackingUniqueIdentifiers.includes(
|
||||||
|
annotation.data?.cachedStats?.TrackingUniqueIdentifier
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!viewport._actors?.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleSpecifier: cs3DToolsTypes.AnnotationStyle.StyleSpecifier = {
|
||||||
|
toolGroupId: this.toolGroupId,
|
||||||
|
toolName: this.getToolName(),
|
||||||
|
viewportId: enabledElement.viewport.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||||
|
const annotation = filteredAnnotations[i];
|
||||||
|
const annotationUID = annotation.annotationUID;
|
||||||
|
const { renderableData } = annotation.data.cachedStats;
|
||||||
|
const { label, cachedStats } = annotation.data;
|
||||||
|
const { referencedImageId } = annotation.metadata;
|
||||||
|
|
||||||
|
styleSpecifier.annotationUID = annotationUID;
|
||||||
|
|
||||||
|
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||||
|
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||||
|
const color =
|
||||||
|
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||||
|
? 'rgb(0, 255, 0)'
|
||||||
|
: this.getStyle('color', styleSpecifier, annotation);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
color,
|
||||||
|
lineDash,
|
||||||
|
lineWidth,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(renderableData).forEach(GraphicType => {
|
||||||
|
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||||
|
|
||||||
|
let renderMethod;
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
renderMethod = this.renderPoint;
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
renderMethod = this.renderMultipoint;
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
renderMethod = this.renderPolyLine;
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE:
|
||||||
|
renderMethod = this.renderEllipse;
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
|
renderMethod = this.renderEllipse;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported GraphicType: ${GraphicType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasCoordinates = renderMethod(
|
||||||
|
svgDrawingHelper,
|
||||||
|
viewport,
|
||||||
|
renderableDataForGraphicType,
|
||||||
|
annotationUID,
|
||||||
|
referencedImageId,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canvasCoordinates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textLines = this._getTextBoxLinesFromLabels(label);
|
||||||
|
|
||||||
|
let canvasCornersToUseForTextBox = canvasCoordinates;
|
||||||
|
|
||||||
|
if (GraphicType === SCOORD_TYPES.ELLIPSE) {
|
||||||
|
canvasCornersToUseForTextBox = utilities.math.ellipse.getCanvasEllipseCorners(
|
||||||
|
canvasCoordinates
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(
|
||||||
|
canvasCornersToUseForTextBox
|
||||||
|
);
|
||||||
|
|
||||||
|
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(
|
||||||
|
canvasTextBoxCoords
|
||||||
|
);
|
||||||
|
|
||||||
|
const textBoxPosition = viewport.worldToCanvas(
|
||||||
|
annotation.data.handles.textBox.worldPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
const textBoxUID = '1';
|
||||||
|
const textBoxOptions = this.getLinkedTextBoxStyle(
|
||||||
|
styleSpecifier,
|
||||||
|
annotation
|
||||||
|
);
|
||||||
|
|
||||||
|
const boundingBox = drawing.drawLinkedTextBox(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
textBoxUID,
|
||||||
|
textLines,
|
||||||
|
textBoxPosition,
|
||||||
|
canvasCoordinates,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
...textBoxOptions,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { x: left, y: top, width, height } = boundingBox;
|
||||||
|
|
||||||
|
annotation.data.handles.textBox.worldBoundingBox = {
|
||||||
|
topLeft: viewport.canvasToWorld([left, top]),
|
||||||
|
topRight: viewport.canvasToWorld([left + width, top]),
|
||||||
|
bottomLeft: viewport.canvasToWorld([left, top + height]),
|
||||||
|
bottomRight: viewport.canvasToWorld([left + width, top + height]),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderPolyLine(
|
||||||
|
svgDrawingHelper,
|
||||||
|
viewport,
|
||||||
|
renderableData,
|
||||||
|
annotationUID,
|
||||||
|
referencedImageId,
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
// Todo: this needs to use the drawPolyLine from cs3D since it is implemented
|
||||||
|
// now, before it was implemented with a loop over drawLine which is hacky
|
||||||
|
|
||||||
|
let canvasCoordinates;
|
||||||
|
renderableData.map((data, index) => {
|
||||||
|
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
|
||||||
|
|
||||||
|
if (canvasCoordinates.length === 2) {
|
||||||
|
const lineUID = `${index}`;
|
||||||
|
drawing.drawLine(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
lineUID,
|
||||||
|
canvasCoordinates[0],
|
||||||
|
canvasCoordinates[1],
|
||||||
|
{
|
||||||
|
color: options.color,
|
||||||
|
width: options.lineWidth,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Drawing polyline for SR not yet implemented');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvasCoordinates; // used for drawing textBox
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMultipoint(
|
||||||
|
svgDrawingHelper,
|
||||||
|
viewport,
|
||||||
|
renderableData,
|
||||||
|
annotationUID,
|
||||||
|
referencedImageId,
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
let canvasCoordinates;
|
||||||
|
renderableData.map((data, index) => {
|
||||||
|
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
|
||||||
|
const handleGroupUID = '0';
|
||||||
|
drawing.drawHandles(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
handleGroupUID,
|
||||||
|
canvasCoordinates,
|
||||||
|
{
|
||||||
|
color: options.color,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPoint(
|
||||||
|
svgDrawingHelper,
|
||||||
|
viewport,
|
||||||
|
renderableData,
|
||||||
|
annotationUID,
|
||||||
|
referencedImageId,
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
const canvasCoordinates = [];
|
||||||
|
renderableData.map((data, index) => {
|
||||||
|
const point = data[0];
|
||||||
|
// This gives us one point for arrow
|
||||||
|
canvasCoordinates.push(viewport.worldToCanvas(point));
|
||||||
|
|
||||||
|
// We get the other point for the arrow by using the image size
|
||||||
|
const imagePixelModule = metaData.get(
|
||||||
|
'imagePixelModule',
|
||||||
|
referencedImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
let xOffset = 10;
|
||||||
|
let yOffset = 10;
|
||||||
|
|
||||||
|
if (imagePixelModule) {
|
||||||
|
const { columns, rows } = imagePixelModule;
|
||||||
|
xOffset = columns / 10;
|
||||||
|
yOffset = rows / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imagePoint = csUtils.worldToImageCoords(referencedImageId, point);
|
||||||
|
const arrowEnd = csUtils.imageToWorldCoords(referencedImageId, [
|
||||||
|
imagePoint[0] + xOffset,
|
||||||
|
imagePoint[1] + yOffset,
|
||||||
|
]);
|
||||||
|
|
||||||
|
canvasCoordinates.push(viewport.worldToCanvas(arrowEnd));
|
||||||
|
|
||||||
|
const arrowUID = `${index}`;
|
||||||
|
|
||||||
|
// Todo: handle drawing probe as probe, currently we are drawing it as an arrow
|
||||||
|
drawing.drawArrow(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
arrowUID,
|
||||||
|
canvasCoordinates[1],
|
||||||
|
canvasCoordinates[0],
|
||||||
|
{
|
||||||
|
color: options.color,
|
||||||
|
width: options.lineWidth,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvasCoordinates; // used for drawing textBox
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEllipse(
|
||||||
|
svgDrawingHelper,
|
||||||
|
viewport,
|
||||||
|
renderableData,
|
||||||
|
annotationUID,
|
||||||
|
referencedImageId,
|
||||||
|
options
|
||||||
|
) {
|
||||||
|
let canvasCoordinates;
|
||||||
|
renderableData.map((data, index) => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
// since oblique ellipse is not supported for hydration right now
|
||||||
|
// we just return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ellipsePointsWorld = data;
|
||||||
|
|
||||||
|
canvasCoordinates = ellipsePointsWorld.map(p =>
|
||||||
|
viewport.worldToCanvas(p)
|
||||||
|
);
|
||||||
|
|
||||||
|
const canvasCorners = <Array<Types.Point2>>(
|
||||||
|
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lineUID = `${index}`;
|
||||||
|
drawing.drawEllipse(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
lineUID,
|
||||||
|
canvasCorners[0],
|
||||||
|
canvasCorners[1],
|
||||||
|
{
|
||||||
|
color: options.color,
|
||||||
|
width: options.lineWidth,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return canvasCoordinates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHORT_HAND_MAP = {
|
||||||
|
'Short Axis': 'W ',
|
||||||
|
'Long Axis': 'L ',
|
||||||
|
AREA: 'Area ',
|
||||||
|
Length: '',
|
||||||
|
CORNERSTONEFREETEXT: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function _labelToShorthand(label) {
|
||||||
|
const shortHand = SHORT_HAND_MAP[label];
|
||||||
|
|
||||||
|
if (shortHand !== undefined) {
|
||||||
|
return shortHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
import { getEnabledElement } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
TrackingUniqueIdentifier: null,
|
||||||
|
trackingIdentifiersByViewportId: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is being used to store the per-viewport state of the SR tools,
|
||||||
|
* Since, all the toolStates are added to the cornerstoneTools, when displaying the SRTools,
|
||||||
|
* if there are two viewports rendering the same imageId, we don't want to show
|
||||||
|
* the same SR annotation twice on irrelevant viewport, hence, we are storing the state
|
||||||
|
* of the SR tools in state here, so that we can filter them later.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function setTrackingUniqueIdentifiersForElement(
|
||||||
|
element,
|
||||||
|
trackingUniqueIdentifiers,
|
||||||
|
activeIndex = 0
|
||||||
|
) {
|
||||||
|
const enabledElement = getEnabledElement(element);
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
state.trackingIdentifiersByViewportId[viewport.id] = {
|
||||||
|
trackingUniqueIdentifiers,
|
||||||
|
activeIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveTrackingUniqueIdentifierForElement(
|
||||||
|
element,
|
||||||
|
TrackingUniqueIdentifier
|
||||||
|
) {
|
||||||
|
const enabledElement = getEnabledElement(element);
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
const trackingIdentifiersForElement =
|
||||||
|
state.trackingIdentifiersByViewportId[viewport.id];
|
||||||
|
|
||||||
|
if (trackingIdentifiersForElement) {
|
||||||
|
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
|
||||||
|
tuid => tuid === TrackingUniqueIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
trackingIdentifiersForElement.activeIndex = activeIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackingUniqueIdentifiersForElement(element) {
|
||||||
|
const enabledElement = getEnabledElement(element);
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
if (state.trackingIdentifiersByViewportId[viewport.id]) {
|
||||||
|
return state.trackingIdentifiersByViewportId[viewport.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { trackingUniqueIdentifiers: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
setTrackingUniqueIdentifiersForElement,
|
||||||
|
setActiveTrackingUniqueIdentifierForElement,
|
||||||
|
getTrackingUniqueIdentifiersForElement,
|
||||||
|
};
|
||||||
15
extensions/cornerstone-dicom-sr/src/tools/toolNames.ts
Normal file
15
extensions/cornerstone-dicom-sr/src/tools/toolNames.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
|
||||||
|
import SRLengthTool from './tools/SRLength';
|
||||||
|
import SRBidirectional from './tools/SRBidirectional';
|
||||||
|
import SREllipticalROI from './tools/SREllipticalROI';
|
||||||
|
import SRArrowAnnotate from './tools/SRArrowAnnotate';
|
||||||
|
|
||||||
|
const toolNames = {
|
||||||
|
DICOMSRDisplay: DICOMSRDisplayTool.toolName,
|
||||||
|
SRLength: SRLengthTool.toolName,
|
||||||
|
SRBidirectional: SRBidirectional.toolName,
|
||||||
|
SREllipticalROI: SREllipticalROI.toolName,
|
||||||
|
SRArrowAnnotate: SRArrowAnnotate.toolName,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default toolNames;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { ArrowAnnotateTool } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we are extending ArrowAnnotateTool is to create a new tool for SR
|
||||||
|
* viewport which basically has a different name. This is done since Cornerstone
|
||||||
|
* has shifted from creating tool instances for each annotation, and we have ArrowAnnotate
|
||||||
|
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
|
||||||
|
* the SR annotation to the MeasurementService (since there is a ArrowAnnotateTool mapper),
|
||||||
|
* but with extending and renaming it, there is not mapper for SRArrowAnnotateTool; hence
|
||||||
|
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
|
||||||
|
*/
|
||||||
|
class SRArrowAnnotateTool extends ArrowAnnotateTool {
|
||||||
|
static toolName = 'SRArrowAnnotate';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SRArrowAnnotateTool;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { BidirectionalTool } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we are extending BidirectionalTool is to create a new tool for SR
|
||||||
|
* viewport which basically has a different name. This is done since Cornerstone
|
||||||
|
* has shifted from creating tool instances for each annotation, and we have Bidirectional
|
||||||
|
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
|
||||||
|
* the SR annotation to the MeasurementService (since there is a BidirectionalTool mapper),
|
||||||
|
* but with extending and renaming it, there is not mapper for SRBidirectionalTool; hence
|
||||||
|
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
|
||||||
|
*/
|
||||||
|
class SRBidirectional extends BidirectionalTool {
|
||||||
|
static toolName = 'SRBidirectional';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SRBidirectional;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { EllipticalROITool } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we are extending EllipticalROITool is to create a new tool for SR
|
||||||
|
* viewport which basically has a different name. This is done since Cornerstone
|
||||||
|
* has shifted from creating tool instances for each annotation, and we have EllipticalROI
|
||||||
|
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
|
||||||
|
* the SR annotation to the MeasurementService (since there is a EllipticalROITool mapper),
|
||||||
|
* but with extending and renaming it, there is not mapper for SREllipticalROITool; hence
|
||||||
|
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
|
||||||
|
*/
|
||||||
|
class SREllipticalROI extends EllipticalROITool {
|
||||||
|
static toolName = 'SREllipticalROI';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SREllipticalROI;
|
||||||
16
extensions/cornerstone-dicom-sr/src/tools/tools/SRLength.ts
Normal file
16
extensions/cornerstone-dicom-sr/src/tools/tools/SRLength.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { LengthTool } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reason we are extending LengthTool is to create a new tool for SR
|
||||||
|
* viewport which basically has a different name. This is done since Cornerstone
|
||||||
|
* has shifted from creating tool instances for each annotation, and we have Length
|
||||||
|
* mappers at the MeasurementService, so if we didn't do this, we would be mapping
|
||||||
|
* the SR annotation to the MeasurementService (since there is a LengthTool mapper),
|
||||||
|
* but with extending and renaming it, there is not mapper for SRLengthTool; hence
|
||||||
|
* no mapping; hence no new measurement, just temporary ones for the SR viewport.
|
||||||
|
*/
|
||||||
|
class SRLengthTool extends LengthTool {
|
||||||
|
static toolName = 'SRLength';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SRLengthTool;
|
||||||
244
extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts
Normal file
244
extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
|
import { Types, annotation } from '@cornerstonejs/tools';
|
||||||
|
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
||||||
|
import toolNames from '../tools/toolNames';
|
||||||
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
|
|
||||||
|
const EPSILON = 1e-4;
|
||||||
|
|
||||||
|
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||||
|
|
||||||
|
export default function addMeasurement(
|
||||||
|
measurement,
|
||||||
|
imageId,
|
||||||
|
displaySetInstanceUID
|
||||||
|
) {
|
||||||
|
// TODO -> Render rotated ellipse .
|
||||||
|
const toolName = toolNames.DICOMSRDisplay;
|
||||||
|
|
||||||
|
const measurementData = {
|
||||||
|
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
|
||||||
|
renderableData: {},
|
||||||
|
labels: measurement.labels,
|
||||||
|
imageId,
|
||||||
|
};
|
||||||
|
|
||||||
|
measurement.coords.forEach(coord => {
|
||||||
|
const { GraphicType, GraphicData } = coord;
|
||||||
|
|
||||||
|
if (measurementData.renderableData[GraphicType] === undefined) {
|
||||||
|
measurementData.renderableData[GraphicType] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
measurementData.renderableData[GraphicType].push(
|
||||||
|
_getRenderableData(
|
||||||
|
GraphicType,
|
||||||
|
GraphicData,
|
||||||
|
imageId,
|
||||||
|
measurement.TrackingIdentifier
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use the metadata provider to grab its imagePlaneModule metadata
|
||||||
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||||
|
|
||||||
|
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
||||||
|
|
||||||
|
const SRAnnotation: Types.Annotation = {
|
||||||
|
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||||
|
metadata: {
|
||||||
|
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||||
|
toolName: toolName,
|
||||||
|
referencedImageId: imageId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: measurement.labels,
|
||||||
|
handles: {
|
||||||
|
textBox: {},
|
||||||
|
},
|
||||||
|
cachedStats: {
|
||||||
|
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||||
|
renderableData: measurementData.renderableData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
annotationManager.addAnnotation(SRAnnotation);
|
||||||
|
|
||||||
|
measurement.loaded = true;
|
||||||
|
measurement.imageId = imageId;
|
||||||
|
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||||
|
|
||||||
|
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
||||||
|
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
||||||
|
// It'd be super weird if it didn't anyway as a SCOORD.
|
||||||
|
measurement.ReferencedSOPInstanceUID =
|
||||||
|
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||||
|
delete measurement.coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getRenderableData(
|
||||||
|
GraphicType,
|
||||||
|
GraphicData,
|
||||||
|
imageId,
|
||||||
|
TrackingIdentifier
|
||||||
|
) {
|
||||||
|
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||||
|
|
||||||
|
let renderableData: csTypes.Point3[];
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
renderableData = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
const worldPos = utilities.imageToWorldCoords(imageId, [
|
||||||
|
GraphicData[i],
|
||||||
|
GraphicData[i + 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderableData.push(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE: {
|
||||||
|
const pointsWorld = [];
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
const worldPos = utilities.imageToWorldCoords(imageId, [
|
||||||
|
GraphicData[i],
|
||||||
|
GraphicData[i + 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
pointsWorld.push(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not have an explicit draw circle svg helper in Cornerstone3D at
|
||||||
|
// this time, but we can use the ellipse svg helper to draw a circle, so
|
||||||
|
// here we reshape the data for that purpose.
|
||||||
|
const center = pointsWorld[0];
|
||||||
|
const onPerimeter = pointsWorld[1];
|
||||||
|
|
||||||
|
const radius = vec3.distance(center, onPerimeter);
|
||||||
|
|
||||||
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||||
|
|
||||||
|
if (!imagePlaneModule) {
|
||||||
|
throw new Error('No imagePlaneModule found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
columnCosines,
|
||||||
|
rowCosines,
|
||||||
|
}: {
|
||||||
|
columnCosines: csTypes.Point3;
|
||||||
|
rowCosines: csTypes.Point3;
|
||||||
|
} = imagePlaneModule;
|
||||||
|
|
||||||
|
// we need to get major/minor axis (which are both the same size major = minor)
|
||||||
|
|
||||||
|
// first axisStart
|
||||||
|
const firstAxisStart = vec3.create();
|
||||||
|
vec3.scaleAndAdd(firstAxisStart, center, columnCosines, radius);
|
||||||
|
|
||||||
|
const firstAxisEnd = vec3.create();
|
||||||
|
vec3.scaleAndAdd(firstAxisEnd, center, columnCosines, -radius);
|
||||||
|
|
||||||
|
// second axisStart
|
||||||
|
const secondAxisStart = vec3.create();
|
||||||
|
vec3.scaleAndAdd(secondAxisStart, center, rowCosines, radius);
|
||||||
|
|
||||||
|
const secondAxisEnd = vec3.create();
|
||||||
|
vec3.scaleAndAdd(secondAxisEnd, center, rowCosines, -radius);
|
||||||
|
|
||||||
|
renderableData = [
|
||||||
|
firstAxisStart as csTypes.Point3,
|
||||||
|
firstAxisEnd as csTypes.Point3,
|
||||||
|
secondAxisStart as csTypes.Point3,
|
||||||
|
secondAxisEnd as csTypes.Point3,
|
||||||
|
];
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SCOORD_TYPES.ELLIPSE: {
|
||||||
|
// GraphicData is ordered as [majorAxisStartX, majorAxisStartY, majorAxisEndX, majorAxisEndY, minorAxisStartX, minorAxisStartY, minorAxisEndX, minorAxisEndY]
|
||||||
|
// But Cornerstone3D points are ordered as top, bottom, left, right for the
|
||||||
|
// ellipse so we need to identify if the majorAxis is horizontal or vertical
|
||||||
|
// and then choose the correct points to use for the ellipse.
|
||||||
|
|
||||||
|
const pointsWorld: csTypes.Point3[] = [];
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
const worldPos = utilities.imageToWorldCoords(imageId, [
|
||||||
|
GraphicData[i],
|
||||||
|
GraphicData[i + 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
pointsWorld.push(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
const majorAxisStart = vec3.fromValues(...pointsWorld[0]);
|
||||||
|
const majorAxisEnd = vec3.fromValues(...pointsWorld[1]);
|
||||||
|
const minorAxisStart = vec3.fromValues(...pointsWorld[2]);
|
||||||
|
const minorAxisEnd = vec3.fromValues(...pointsWorld[3]);
|
||||||
|
|
||||||
|
const majorAxisVec = vec3.create();
|
||||||
|
vec3.sub(majorAxisVec, majorAxisEnd, majorAxisStart);
|
||||||
|
|
||||||
|
// normalize majorAxisVec to avoid scaling issues
|
||||||
|
vec3.normalize(majorAxisVec, majorAxisVec);
|
||||||
|
|
||||||
|
const minorAxisVec = vec3.create();
|
||||||
|
vec3.sub(minorAxisVec, minorAxisEnd, minorAxisStart);
|
||||||
|
vec3.normalize(minorAxisVec, minorAxisVec);
|
||||||
|
|
||||||
|
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||||
|
|
||||||
|
if (!imagePlaneModule) {
|
||||||
|
throw new Error('imageId does not have imagePlaneModule metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
columnCosines,
|
||||||
|
}: { columnCosines: csTypes.Point3 } = imagePlaneModule;
|
||||||
|
|
||||||
|
// find which axis is parallel to the columnCosines
|
||||||
|
const columnCosinesVec = vec3.fromValues(...columnCosines);
|
||||||
|
|
||||||
|
const projectedMajorAxisOnColVec = Math.abs(
|
||||||
|
vec3.dot(columnCosinesVec, majorAxisVec)
|
||||||
|
);
|
||||||
|
const projectedMinorAxisOnColVec = Math.abs(
|
||||||
|
vec3.dot(columnCosinesVec, minorAxisVec)
|
||||||
|
);
|
||||||
|
|
||||||
|
const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec);
|
||||||
|
const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec);
|
||||||
|
|
||||||
|
renderableData = [];
|
||||||
|
if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) {
|
||||||
|
renderableData = [
|
||||||
|
pointsWorld[0],
|
||||||
|
pointsWorld[1],
|
||||||
|
pointsWorld[2],
|
||||||
|
pointsWorld[3],
|
||||||
|
];
|
||||||
|
} else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) {
|
||||||
|
renderableData = [
|
||||||
|
pointsWorld[2],
|
||||||
|
pointsWorld[3],
|
||||||
|
pointsWorld[0],
|
||||||
|
pointsWorld[1],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.warn('Unsupported GraphicType:', GraphicType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderableData;
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import { annotation } from '@cornerstonejs/tools';
|
||||||
|
const { log } = OHIF;
|
||||||
|
|
||||||
|
function getFilteredCornerstoneToolState(
|
||||||
|
measurementData,
|
||||||
|
additionalFindingTypes
|
||||||
|
) {
|
||||||
|
const filteredToolState = {};
|
||||||
|
|
||||||
|
function addToFilteredToolState(annotation, toolType) {
|
||||||
|
if (!annotation.metadata?.referencedImageId) {
|
||||||
|
log.warn(
|
||||||
|
`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageId = annotation.metadata.referencedImageId;
|
||||||
|
|
||||||
|
if (!filteredToolState[imageId]) {
|
||||||
|
filteredToolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdSpecificToolState = filteredToolState[imageId];
|
||||||
|
|
||||||
|
if (!imageIdSpecificToolState[toolType]) {
|
||||||
|
imageIdSpecificToolState[toolType] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurementDataI = measurementData.find(
|
||||||
|
md => md.uid === annotation.annotationUID
|
||||||
|
);
|
||||||
|
const toolData = imageIdSpecificToolState[toolType].data;
|
||||||
|
|
||||||
|
let finding;
|
||||||
|
const findingSites = [];
|
||||||
|
|
||||||
|
// NOTE -> We use the CORNERSTONEJS coding schemeDesignator which we have
|
||||||
|
// defined in the dcmjs adapters
|
||||||
|
if (measurementDataI.label) {
|
||||||
|
if (additionalFindingTypes.includes(toolType)) {
|
||||||
|
finding = {
|
||||||
|
CodeValue: 'CORNERSTONEFREETEXT',
|
||||||
|
CodingSchemeDesignator: 'CORNERSTONEJS',
|
||||||
|
CodeMeaning: measurementDataI.label,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
findingSites.push({
|
||||||
|
CodeValue: 'CORNERSTONEFREETEXT',
|
||||||
|
CodingSchemeDesignator: 'CORNERSTONEJS',
|
||||||
|
CodeMeaning: measurementDataI.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurement = Object.assign({}, annotation, {
|
||||||
|
finding,
|
||||||
|
findingSites,
|
||||||
|
});
|
||||||
|
|
||||||
|
toolData.push(measurement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uidFilter = measurementData.map(md => md.uid);
|
||||||
|
const uids = uidFilter.slice();
|
||||||
|
|
||||||
|
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
||||||
|
const framesOfReference = annotationManager.getFramesOfReference();
|
||||||
|
|
||||||
|
for (let i = 0; i < framesOfReference.length; i++) {
|
||||||
|
const frameOfReference = framesOfReference[i];
|
||||||
|
|
||||||
|
const frameOfReferenceAnnotations = annotationManager.getFrameOfReferenceAnnotations(
|
||||||
|
frameOfReference
|
||||||
|
);
|
||||||
|
|
||||||
|
const toolTypes = Object.keys(frameOfReferenceAnnotations);
|
||||||
|
|
||||||
|
for (let j = 0; j < toolTypes.length; j++) {
|
||||||
|
const toolType = toolTypes[j];
|
||||||
|
|
||||||
|
const annotations = frameOfReferenceAnnotations[toolType];
|
||||||
|
|
||||||
|
if (annotations) {
|
||||||
|
for (let k = 0; k < annotations.length; k++) {
|
||||||
|
const annotation = annotations[k];
|
||||||
|
const uidIndex = uids.findIndex(
|
||||||
|
uid => uid === annotation.annotationUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uidIndex !== -1) {
|
||||||
|
addToFilteredToolState(annotation, toolType);
|
||||||
|
uids.splice(uidIndex, 1);
|
||||||
|
|
||||||
|
if (!uids.length) {
|
||||||
|
return filteredToolState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredToolState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getFilteredCornerstoneToolState;
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import { adapters } from 'dcmjs';
|
import { adapters } from 'dcmjs';
|
||||||
|
|
||||||
const cornerstoneAdapters = adapters.Cornerstone;
|
const cornerstoneAdapters = adapters.Cornerstone3D;
|
||||||
|
|
||||||
|
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||||
|
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`.
|
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`.
|
||||||
@ -14,7 +17,7 @@ export default function isRehydratable(displaySet, mappings) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappingDefinitions = mappings.map(m => m.definition);
|
const mappingDefinitions = mappings.map(m => m.annotationType);
|
||||||
const { measurements } = displaySet;
|
const { measurements } = displaySet;
|
||||||
|
|
||||||
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||||
@ -35,9 +38,18 @@ export default function isRehydratable(displaySet, mappings) {
|
|||||||
|
|
||||||
for (let i = 0; i < measurements.length; i++) {
|
for (let i = 0; i < measurements.length; i++) {
|
||||||
const TrackingIdentifier = measurements[i].TrackingIdentifier;
|
const TrackingIdentifier = measurements[i].TrackingIdentifier;
|
||||||
const hydratable = adapters.some(adapter =>
|
const hydratable = adapters.some(adapter => {
|
||||||
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
|
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||||
);
|
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
|
||||||
|
cornerstoneTag = CORNERSTONE_3D_TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
|
||||||
|
|
||||||
|
return adapter.isValidCornerstoneTrackingIdentifier(
|
||||||
|
mappedTrackingIdentifier
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (hydratable) {
|
if (hydratable) {
|
||||||
return true;
|
return true;
|
||||||
@ -7,7 +7,7 @@ import dcmjs from 'dcmjs';
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
const isToolSupported = toolName => {
|
const isToolSupported = toolName => {
|
||||||
const adapter = dcmjs.adapters.Cornerstone;
|
const adapter = dcmjs.adapters.Cornerstone3D;
|
||||||
return !!adapter[toolName];
|
return !!adapter[toolName];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -0,0 +1,452 @@
|
|||||||
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import OHIF, { utils } from '@ohif/core';
|
||||||
|
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||||
|
import {
|
||||||
|
Notification,
|
||||||
|
ViewportActionBar,
|
||||||
|
useViewportGrid,
|
||||||
|
useViewportDialog,
|
||||||
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
const { formatDate } = utils;
|
||||||
|
|
||||||
|
const MEASUREMENT_TRACKING_EXTENSION_ID =
|
||||||
|
'@ohif/extension-measurement-tracking';
|
||||||
|
|
||||||
|
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||||
|
|
||||||
|
function OHIFCornerstoneSRViewport(props) {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
dataSource,
|
||||||
|
displaySets,
|
||||||
|
viewportIndex,
|
||||||
|
viewportLabel,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
// SR viewport will always have a single display set
|
||||||
|
if (displaySets.length > 1) {
|
||||||
|
throw new Error('SR viewport should only have a single display set');
|
||||||
|
}
|
||||||
|
|
||||||
|
const srDisplaySet = displaySets[0];
|
||||||
|
|
||||||
|
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||||
|
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||||
|
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||||
|
const [measurementCount, setMeasurementCount] = useState(1);
|
||||||
|
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
referencedDisplaySetMetadata,
|
||||||
|
setReferencedDisplaySetMetadata,
|
||||||
|
] = useState(null);
|
||||||
|
const [isHydrated, setIsHydrated] = useState(srDisplaySet.isHydrated);
|
||||||
|
const [element, setElement] = useState(null);
|
||||||
|
const { viewports, activeViewportIndex } = viewportGrid;
|
||||||
|
|
||||||
|
// Optional hook into tracking extension, if present.
|
||||||
|
let trackedMeasurements;
|
||||||
|
let sendTrackedMeasurementsEvent;
|
||||||
|
|
||||||
|
const hasMeasurementTrackingExtension = extensionManager.registeredExtensionIds.includes(
|
||||||
|
MEASUREMENT_TRACKING_EXTENSION_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: this is a hook that fails if we register/de-register
|
||||||
|
if (hasMeasurementTrackingExtension) {
|
||||||
|
const contextModule = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-measurement-tracking.contextModule.TrackedMeasurementsContext'
|
||||||
|
);
|
||||||
|
|
||||||
|
const useTrackedMeasurements = () => useContext(contextModule.context);
|
||||||
|
|
||||||
|
[
|
||||||
|
trackedMeasurements,
|
||||||
|
sendTrackedMeasurementsEvent,
|
||||||
|
] = useTrackedMeasurements();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the tracking identifiers per viewport in order to be able to
|
||||||
|
* show the SR measurements on the referenced image on the correct viewport,
|
||||||
|
* when multiple viewports are used.
|
||||||
|
*/
|
||||||
|
const setTrackingIdentifiers = useCallback(
|
||||||
|
measurementSelected => {
|
||||||
|
const { measurements } = srDisplaySet;
|
||||||
|
|
||||||
|
setTrackingUniqueIdentifiersForElement(
|
||||||
|
element,
|
||||||
|
measurements.map(measurement => measurement.TrackingUniqueIdentifier),
|
||||||
|
measurementSelected
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[element, measurementSelected, srDisplaySet]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||||
|
* has enabled the element. Note: we delegate all the image rendering to
|
||||||
|
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||||
|
* the image rendering, element enabling etc.
|
||||||
|
*/
|
||||||
|
const onElementEnabled = evt => {
|
||||||
|
setElement(evt.detail.element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateViewport = useCallback(
|
||||||
|
newMeasurementSelected => {
|
||||||
|
const {
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
sopClassUids,
|
||||||
|
} = srDisplaySet;
|
||||||
|
|
||||||
|
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sopClassUids && sopClassUids.length > 1) {
|
||||||
|
// Todo: what happens if there are multiple SOP Classes? Why we are
|
||||||
|
// not throwing an error?
|
||||||
|
console.warn(
|
||||||
|
'More than one SOPClassUID in the same series is not yet supported.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getViewportReferencedDisplaySetData(
|
||||||
|
srDisplaySet,
|
||||||
|
newMeasurementSelected,
|
||||||
|
DisplaySetService
|
||||||
|
).then(({ referencedDisplaySet, referencedDisplaySetMetadata }) => {
|
||||||
|
setMeasurementSelected(newMeasurementSelected);
|
||||||
|
setActiveImageDisplaySetData(referencedDisplaySet);
|
||||||
|
setReferencedDisplaySetMetadata(referencedDisplaySetMetadata);
|
||||||
|
|
||||||
|
if (
|
||||||
|
referencedDisplaySet.displaySetInstanceUID ===
|
||||||
|
activeImageDisplaySetData?.displaySetInstanceUID
|
||||||
|
) {
|
||||||
|
const { measurements } = srDisplaySet;
|
||||||
|
|
||||||
|
// it means that we have a new referenced display set, and the
|
||||||
|
// imageIdIndex will handle it by updating the viewport, but if they
|
||||||
|
// are the same we just need to use MeasurementService to jump to the
|
||||||
|
// new measurement
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const csViewport = CornerstoneViewportService.getCornerstoneViewport(
|
||||||
|
viewportInfo.getViewportId()
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageIds = csViewport.getImageIds();
|
||||||
|
|
||||||
|
const imageIdIndex = imageIds.indexOf(
|
||||||
|
measurements[newMeasurementSelected].imageId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imageIdIndex !== -1) {
|
||||||
|
csViewport.setImageIdIndex(imageIdIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getCornerstoneViewport = useCallback(() => {
|
||||||
|
if (!activeImageDisplaySetData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { component: Component } = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { measurements } = srDisplaySet;
|
||||||
|
const measurement = measurements[measurementSelected];
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialImageIndex = activeImageDisplaySetData.images.findIndex(
|
||||||
|
image => image.imageId === measurement.imageId
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
// should be passed second since we don't want SR displaySet to
|
||||||
|
// override the activeImageDisplaySetData
|
||||||
|
displaySets={[activeImageDisplaySetData]}
|
||||||
|
viewportOptions={{
|
||||||
|
toolGroupId: `${SR_TOOLGROUP_BASE_NAME}`,
|
||||||
|
}}
|
||||||
|
onElementEnabled={onElementEnabled}
|
||||||
|
initialImageIndex={initialImageIndex}
|
||||||
|
></Component>
|
||||||
|
);
|
||||||
|
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
|
||||||
|
|
||||||
|
const onMeasurementChange = useCallback(
|
||||||
|
direction => {
|
||||||
|
let newMeasurementSelected = measurementSelected;
|
||||||
|
|
||||||
|
if (direction === 'right') {
|
||||||
|
newMeasurementSelected++;
|
||||||
|
|
||||||
|
if (newMeasurementSelected >= measurementCount) {
|
||||||
|
newMeasurementSelected = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newMeasurementSelected--;
|
||||||
|
|
||||||
|
if (newMeasurementSelected < 0) {
|
||||||
|
newMeasurementSelected = measurementCount - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTrackingIdentifiers(newMeasurementSelected);
|
||||||
|
updateViewport(newMeasurementSelected);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
measurementSelected,
|
||||||
|
measurementCount,
|
||||||
|
updateViewport,
|
||||||
|
setTrackingIdentifiers,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
Cleanup the SR viewport when the viewport is destroyed
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe(
|
||||||
|
DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||||
|
({ displaySetInstanceUIDs }) => {
|
||||||
|
const activeViewport = viewports[activeViewportIndex];
|
||||||
|
if (
|
||||||
|
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
|
||||||
|
) {
|
||||||
|
viewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportIndex: activeViewportIndex,
|
||||||
|
displaySetInstanceUIDs: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading the measurements from the SR viewport, which goes through the
|
||||||
|
* isHydratable check, the outcome for the isHydrated state here is always FALSE
|
||||||
|
* since we don't do the hydration here. Todo: can't we just set it as false? why
|
||||||
|
* we are changing the state here? isHydrated is always false at this stage, and
|
||||||
|
* if it is hydrated we don't event use the SR viewport.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!srDisplaySet.isLoaded) {
|
||||||
|
srDisplaySet.load();
|
||||||
|
}
|
||||||
|
setIsHydrated(srDisplaySet.isHydrated);
|
||||||
|
|
||||||
|
const numMeasurements = srDisplaySet.measurements.length;
|
||||||
|
setMeasurementCount(numMeasurements);
|
||||||
|
}, [srDisplaySet]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to update the tracking identifiers when the selected measurement changes or
|
||||||
|
* the element changes
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!element || !srDisplaySet.isLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTrackingIdentifiers(measurementSelected);
|
||||||
|
}, [measurementSelected, element, setTrackingIdentifiers, srDisplaySet]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||||
|
* it is updating a local variable? which is not state.
|
||||||
|
*/
|
||||||
|
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||||
|
useEffect(() => {
|
||||||
|
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||||
|
}, [trackedMeasurements]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data fetching for the SR displaySet, which updates the measurements and
|
||||||
|
* also gets the referenced image displaySet that SR is based on.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
updateViewport(measurementSelected);
|
||||||
|
}, [dataSource, srDisplaySet]);
|
||||||
|
|
||||||
|
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
let childrenWithProps = null;
|
||||||
|
|
||||||
|
if (!activeImageDisplaySetData || !referencedDisplaySetMetadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children && children.length) {
|
||||||
|
childrenWithProps = children.map((child, index) => {
|
||||||
|
return (
|
||||||
|
child &&
|
||||||
|
React.cloneElement(child, {
|
||||||
|
viewportIndex,
|
||||||
|
key: index,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Modality } = srDisplaySet;
|
||||||
|
|
||||||
|
const {
|
||||||
|
PatientID,
|
||||||
|
PatientName,
|
||||||
|
PatientSex,
|
||||||
|
PatientAge,
|
||||||
|
SliceThickness,
|
||||||
|
ManufacturerModelName,
|
||||||
|
StudyDate,
|
||||||
|
SeriesDescription,
|
||||||
|
SpacingBetweenSlices,
|
||||||
|
SeriesNumber,
|
||||||
|
} = referencedDisplaySetMetadata;
|
||||||
|
|
||||||
|
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewportActionBar
|
||||||
|
onDoubleClick={evt => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
|
}}
|
||||||
|
onPillClick={() => {
|
||||||
|
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
|
||||||
|
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||||
|
viewportIndex,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onSeriesChange={onMeasurementChange}
|
||||||
|
studyData={{
|
||||||
|
label: viewportLabel,
|
||||||
|
useAltStyling: true,
|
||||||
|
isTracked: false,
|
||||||
|
isLocked,
|
||||||
|
isRehydratable: srDisplaySet.isRehydratable,
|
||||||
|
isHydrated,
|
||||||
|
studyDate: formatDate(StudyDate),
|
||||||
|
currentSeries: SeriesNumber,
|
||||||
|
seriesDescription: SeriesDescription,
|
||||||
|
modality: Modality,
|
||||||
|
patientInformation: {
|
||||||
|
patientName: PatientName
|
||||||
|
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||||
|
: '',
|
||||||
|
patientSex: PatientSex || '',
|
||||||
|
patientAge: PatientAge || '',
|
||||||
|
MRN: PatientID || '',
|
||||||
|
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||||
|
spacing:
|
||||||
|
SpacingBetweenSlices !== undefined
|
||||||
|
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||||
|
: '',
|
||||||
|
scanner: ManufacturerModelName || '',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||||
|
{getCornerstoneViewport()}
|
||||||
|
<div className="absolute w-full">
|
||||||
|
{viewportDialogState.viewportIndex === viewportIndex && (
|
||||||
|
<Notification
|
||||||
|
message={viewportDialogState.message}
|
||||||
|
type={viewportDialogState.type}
|
||||||
|
actions={viewportDialogState.actions}
|
||||||
|
onSubmit={viewportDialogState.onSubmit}
|
||||||
|
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{childrenWithProps}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OHIFCornerstoneSRViewport.propTypes = {
|
||||||
|
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
|
dataSource: PropTypes.object,
|
||||||
|
children: PropTypes.node,
|
||||||
|
customProps: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
OHIFCornerstoneSRViewport.defaultProps = {
|
||||||
|
customProps: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function _getViewportReferencedDisplaySetData(
|
||||||
|
displaySet,
|
||||||
|
measurementSelected,
|
||||||
|
DisplaySetService
|
||||||
|
) {
|
||||||
|
const { measurements } = displaySet;
|
||||||
|
const measurement = measurements[measurementSelected];
|
||||||
|
|
||||||
|
const { displaySetInstanceUID } = measurement;
|
||||||
|
|
||||||
|
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
|
||||||
|
displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const image0 = referencedDisplaySet.images[0];
|
||||||
|
const referencedDisplaySetMetadata = {
|
||||||
|
PatientID: image0.PatientID,
|
||||||
|
PatientName: image0.PatientName,
|
||||||
|
PatientSex: image0.PatientSex,
|
||||||
|
PatientAge: image0.PatientAge,
|
||||||
|
SliceThickness: image0.SliceThickness,
|
||||||
|
StudyDate: image0.StudyDate,
|
||||||
|
SeriesDescription: image0.SeriesDescription,
|
||||||
|
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||||
|
SeriesNumber: image0.SeriesNumber,
|
||||||
|
ManufacturerModelName: image0.ManufacturerModelName,
|
||||||
|
SpacingBetweenSlices: image0.SpacingBetweenSlices,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { referencedDisplaySetMetadata, referencedDisplaySet };
|
||||||
|
}
|
||||||
|
|
||||||
|
// function _onDoubleClick() {
|
||||||
|
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||||
|
// 'manipulatorState'
|
||||||
|
// ).setters.cancelActiveManipulatorsForElement;
|
||||||
|
// const enabledElements = cornerstoneTools.store.state.enabledElements;
|
||||||
|
// enabledElements.forEach(element => {
|
||||||
|
// cancelActiveManipulatorsForElement(element);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default OHIFCornerstoneSRViewport;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
const SRC_DIR = path.join(__dirname, '../src');
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
const DIST_DIR = path.join(__dirname, '../dist');
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const { merge } = require('webpack-merge');
|
const merge = require('webpack-merge');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
const pkg = require('./../package.json');
|
const pkg = require('./../package.json');
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
||||||
|
|
||||||
const ROOT_DIR = path.join(__dirname, './..');
|
const ROOT_DIR = path.join(__dirname, './..');
|
||||||
const SRC_DIR = path.join(__dirname, '../src');
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
@ -13,7 +12,6 @@ module.exports = (env, argv) => {
|
|||||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
|
||||||
return merge(commonConfig, {
|
return merge(commonConfig, {
|
||||||
devtool: 'source-map',
|
|
||||||
stats: {
|
stats: {
|
||||||
colors: true,
|
colors: true,
|
||||||
hash: true,
|
hash: true,
|
||||||
@ -40,10 +38,6 @@ module.exports = (env, argv) => {
|
|||||||
new webpack.optimize.LimitChunkCountPlugin({
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
maxChunks: 1,
|
maxChunks: 1,
|
||||||
}),
|
}),
|
||||||
new MiniCssExtractPlugin({
|
|
||||||
filename: './dist/[name].css',
|
|
||||||
chunkFilename: './dist/[id].css',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,593 +0,0 @@
|
|||||||
# Change Log
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
||||||
|
|
||||||
## [2.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.2...@ohif/extension-cornerstone@2.7.3) (2020-05-12)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 Fix seg color load ([#1724](https://github.com/OHIF/Viewers/issues/1724)) ([c4f84b1](https://github.com/OHIF/Viewers/commit/c4f84b1174d04ba84d37ed89b6d7ab541be28181))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.1...@ohif/extension-cornerstone@2.7.2) (2020-05-04)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 Proper error handling for derived display sets ([#1708](https://github.com/OHIF/Viewers/issues/1708)) ([5b20d8f](https://github.com/OHIF/Viewers/commit/5b20d8f323e4b3ef9988f2f2ab672d697b6da409))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.7.0...@ohif/extension-cornerstone@2.7.1) (2020-05-04)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.1...@ohif/extension-cornerstone@2.7.0) (2020-04-24)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 🎸 Seg jump to slice + show/hide ([835f64d](https://github.com/OHIF/Viewers/commit/835f64d47a9994f6a25aaf3941a4974e215e7e7f))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.6.0...@ohif/extension-cornerstone@2.6.1) (2020-04-23)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 Multiframe fix ([#1661](https://github.com/OHIF/Viewers/issues/1661)) ([7120561](https://github.com/OHIF/Viewers/commit/71205618ecb8b592247c5acb32284bfe7e18fce5))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.2...@ohif/extension-cornerstone@2.6.0) (2020-04-23)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.3...@ohif/extension-cornerstone@2.5.0) (2020-03-13)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.2...@ohif/extension-cornerstone@2.4.3) (2020-03-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.1...@ohif/extension-cornerstone@2.4.2) (2020-03-09)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Remove Eraser and ROI Window ([6c950a9](https://github.com/OHIF/Viewers/commit/6c950a9669f7fbf3c46e48679fa26ee514824156))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.0...@ohif/extension-cornerstone@2.4.1) (2020-03-03)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* bump react-cornerstone-viewport version to address critical issue ([#1473](https://github.com/OHIF/Viewers/issues/1473)) ([ee80e02](https://github.com/OHIF/Viewers/commit/ee80e026610442e94caf5e4e3e4d193220cd0ece))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.1...@ohif/extension-cornerstone@2.4.0) (2020-02-20)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.0...@ohif/extension-cornerstone@2.3.1) (2020-02-14)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 add WwwcRegionTool to cornerstone tools initialization ([#1302](https://github.com/OHIF/Viewers/issues/1302)) ([d5bf728](https://github.com/OHIF/Viewers/commit/d5bf72851a32dff9fd3fc09332ea5250bc7e6114))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
|
|
||||||
|
|
||||||
|
|
||||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
|
||||||
|
|
||||||
|
|
||||||
### BREAKING CHANGES
|
|
||||||
|
|
||||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.6.0...@ohif/extension-cornerstone@1.7.0) (2019-11-25)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.1...@ohif/extension-cornerstone@1.5.0) (2019-11-13)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.0...@ohif/extension-cornerstone@1.4.1) (2019-11-08)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.1...@ohif/extension-cornerstone@1.4.0) (2019-10-26)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.0...@ohif/extension-cornerstone@1.3.1) (2019-10-09)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 set tools bidirectional, eraser and annotate command ([#1020](https://github.com/OHIF/Viewers/issues/1020)) ([a28984e](https://github.com/OHIF/Viewers/commit/a28984e))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.5...@ohif/extension-cornerstone@1.3.0) (2019-10-09)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.4...@ohif/extension-cornerstone@1.2.5) (2019-09-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* version bump issue ([#963](https://github.com/OHIF/Viewers/issues/963)) ([e607ed2](https://github.com/OHIF/Viewers/commit/e607ed2)), closes [#962](https://github.com/OHIF/Viewers/issues/962)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
|
||||||
* version bump issue ([#962](https://github.com/OHIF/Viewers/issues/962)) ([c80ea17](https://github.com/OHIF/Viewers/commit/c80ea17))
|
|
||||||
|
|
||||||
|
|
||||||
### BREAKING CHANGES
|
|
||||||
|
|
||||||
* DICOM Seg
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.2...@ohif/extension-cornerstone@2.0.0) (2019-09-27)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
|
||||||
|
|
||||||
|
|
||||||
### BREAKING CHANGES
|
|
||||||
|
|
||||||
* DICOM Seg
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.1...@ohif/extension-cornerstone@1.2.2) (2019-09-26)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.0...@ohif/extension-cornerstone@1.2.1) (2019-09-17)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.1...@ohif/extension-cornerstone@1.2.0) (2019-09-12)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **Annotate:** Add annotate tool back to toolbar ([26be967](https://github.com/OHIF/Viewers/commit/26be967))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.1.0...@ohif/extension-cornerstone@1.1.1) (2019-09-12)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.1...@ohif/extension-cornerstone@1.1.0) (2019-09-12)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* **BidirectionalTool:** Add BidrectionalTool to "more" menu ([#911](https://github.com/OHIF/Viewers/issues/911)) ([e40cbae](https://github.com/OHIF/Viewers/commit/e40cbae))
|
|
||||||
* **EraserTool:** add eraserTool to @ohif/extension-cornerstone ([#912](https://github.com/OHIF/Viewers/issues/912)) ([698d274](https://github.com/OHIF/Viewers/commit/698d274))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.0.0...@ohif/extension-cornerstone@1.0.1) (2019-09-10)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.5...@ohif/extension-cornerstone@1.0.0) (2019-09-09)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 🎸 Upgraded to cornerstoneTools 4.0 ([86adb51](https://github.com/OHIF/Viewers/commit/86adb51))
|
|
||||||
|
|
||||||
|
|
||||||
### BREAKING CHANGES
|
|
||||||
|
|
||||||
* n
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.4...@ohif/extension-cornerstone@0.50.5) (2019-09-06)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.3...@ohif/extension-cornerstone@0.50.4) (2019-09-04)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.2...@ohif/extension-cornerstone@0.50.3) (2019-09-04)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.1...@ohif/extension-cornerstone@0.50.2) (2019-09-04)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.50.0-alpha.10...@ohif/extension-cornerstone@0.50.1) (2019-08-14)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.9...@ohif/extension-cornerstone@0.50.0-alpha.10) (2019-08-14)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.0.39-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.8...@ohif/extension-cornerstone@0.0.39-alpha.9) (2019-08-14)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 0.0.39-alpha.8 (2019-08-14)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 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.39-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.6...@ohif/extension-cornerstone@0.0.39-alpha.7) (2019-08-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
# 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.39-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.5...@ohif/extension-cornerstone@0.0.39-alpha.6) (2019-08-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
# 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.39-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.4...@ohif/extension-cornerstone@0.0.39-alpha.5) (2019-08-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
# 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.39-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.3...@ohif/extension-cornerstone@0.0.39-alpha.4) (2019-08-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
# 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.39-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.2...@ohif/extension-cornerstone@0.0.39-alpha.3) (2019-08-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
# 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.39-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.1...@ohif/extension-cornerstone@0.0.39-alpha.2) (2019-08-07)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
## [0.0.39-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@0.0.39-alpha.0...@ohif/extension-cornerstone@0.0.39-alpha.1) (2019-08-07)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
|
|
||||||
## 0.0.39-alpha.0 (2019-08-05)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
|
||||||
@ -1,123 +1 @@
|
|||||||
# @ohif/extension-cornerstone
|
# Cornerstone
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
This extension adds support for viewing and manipulating 2D medical images via a
|
|
||||||
viewport. The underlying implementation wraps the
|
|
||||||
`cornerstonejs/react-cornerstone-viewport`, and provides basic commands and
|
|
||||||
toolbar buttons for common actions.
|
|
||||||
|
|
||||||
<!-- TODO: Simple image or GIF? -->
|
|
||||||
|
|
||||||
#### Index
|
|
||||||
|
|
||||||
Extension Id: `cornerstone`
|
|
||||||
|
|
||||||
- [Commands Module](#commands-module)
|
|
||||||
- [Toolbar Module](#toolbar-module)
|
|
||||||
- [Viewport Module](#viewport-module)
|
|
||||||
|
|
||||||
## Commands Module
|
|
||||||
|
|
||||||
This extensions includes the following `Commands` and `Command Definitions`.
|
|
||||||
These can be registered with `@ohif/core`'s `CommandManager`. After registering
|
|
||||||
the commands, they can be bound to `hotkeys` using the `HotkeysManager` and
|
|
||||||
listed in the `UserPreferences` modal.
|
|
||||||
|
|
||||||
You can read more about [`Commands`][docs-commands], [`Hotkeys`][docs-hotkeys],
|
|
||||||
and the [`UserPreferences` Modal][docs-userprefs] in their respective locations
|
|
||||||
in the OHIF Viewer's documentation.
|
|
||||||
|
|
||||||
| Command Name | Description | Store Contexts |
|
|
||||||
| ---------------------------- | --------------------------------------- | -------------- |
|
|
||||||
| `rotateViewportCW` | | viewports |
|
|
||||||
| `rotateViewportCCW` | | viewports |
|
|
||||||
| `invertViewport` | | viewports |
|
|
||||||
| `flipViewportVertical` | | viewports |
|
|
||||||
| `flipViewportHorizontal` | | viewports |
|
|
||||||
| `scaleUpViewport` | | viewports |
|
|
||||||
| `scaleDownViewport` | | viewports |
|
|
||||||
| `fitViewportToWindow` | | viewports |
|
|
||||||
| `resetViewport` | | viewports |
|
|
||||||
| clearAnnotations | TODO | |
|
|
||||||
| next/previous Image | TODO | |
|
|
||||||
| first/last Image | TODO | |
|
|
||||||
| `nextViewportDisplaySet` | | |
|
|
||||||
| `previousViewportDisplaySet` | | |
|
|
||||||
| `setToolActive` | Activates tool for primary button/touch | |
|
|
||||||
|
|
||||||
## Toolbar Module
|
|
||||||
|
|
||||||
Our toolbar module contains definitions for:
|
|
||||||
|
|
||||||
- `StackScroll`
|
|
||||||
- `Zoom`
|
|
||||||
- `Wwwc`
|
|
||||||
- `Pan`
|
|
||||||
- `Length`
|
|
||||||
- `Angle`
|
|
||||||
- `Reset`
|
|
||||||
- `Cine`
|
|
||||||
|
|
||||||
All use the `ACTIVE_VIEWPORT::CORNERSTONE` context.
|
|
||||||
|
|
||||||
## Viewport Module
|
|
||||||
|
|
||||||
Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport].
|
|
||||||
This module is the most prone to change as we hammer out our Viewport interface.
|
|
||||||
|
|
||||||
## Tool Configuration
|
|
||||||
|
|
||||||
Tools can be configured through extension configuration using the tools key:
|
|
||||||
|
|
||||||
```js
|
|
||||||
...
|
|
||||||
cornerstoneExtensionConfig: {
|
|
||||||
tools: {
|
|
||||||
ArrowAnnotate: {
|
|
||||||
configuration: {
|
|
||||||
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Annotate Tools Configuration
|
|
||||||
|
|
||||||
_We currently support one property for annotation tools._
|
|
||||||
|
|
||||||
### Hide handles
|
|
||||||
|
|
||||||
This extension configuration allows you to toggle on/off handle rendering for
|
|
||||||
all annotate tools:
|
|
||||||
|
|
||||||
```js
|
|
||||||
...
|
|
||||||
cornerstoneExtensionConfig: {
|
|
||||||
hideHandles: true,
|
|
||||||
},
|
|
||||||
...
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
### Repositories
|
|
||||||
|
|
||||||
- [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
|
||||||
- [cornerstonejs/cornerstoneTools][cornerstone-tools]
|
|
||||||
- [cornerstonejs/cornerstone][cornerstone]
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[docs-commands]: https://www.com
|
|
||||||
[docs-hotkeys]: https://www.com
|
|
||||||
[docs-userprefs]: htt
|
|
||||||
[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport
|
|
||||||
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
|
||||||
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
```
|
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "OHIF/Viewers",
|
"repository": "OHIF/Viewers",
|
||||||
"main": "dist/index.umd.js",
|
"main": "dist/index.umd.js",
|
||||||
"module": "src/index.js",
|
"module": "src/index.tsx",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14",
|
"node": ">=10",
|
||||||
"npm": ">=6",
|
"npm": ">=6",
|
||||||
"yarn": ">=1.16.0"
|
"yarn": ">=1.16.0"
|
||||||
},
|
},
|
||||||
@ -16,9 +16,6 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
"keywords": [
|
|
||||||
"ohif-extension"
|
|
||||||
],
|
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
@ -27,29 +24,33 @@
|
|||||||
"dev:cornerstone": "yarn run dev",
|
"dev:cornerstone": "yarn run dev",
|
||||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
"build:package": "yarn run build",
|
"build:package": "yarn run build",
|
||||||
"start": "yarn run dev",
|
"start": "yarn run dev"
|
||||||
"test:unit": "jest --watchAll",
|
|
||||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"cornerstone-core": "2.6.0",
|
"cornerstone-wado-image-loader": "^4.2.0",
|
||||||
"cornerstone-math": "0.1.9",
|
"dcmjs": "^0.24.5",
|
||||||
"cornerstone-tools": "6.0.2",
|
|
||||||
"cornerstone-wado-image-loader": "4.0.4",
|
|
||||||
"dcmjs": "0.16.1",
|
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-resize-detector": "^3.4.0"
|
"react-resize-detector": "^6.7.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.3",
|
"@babel/runtime": "7.17.9",
|
||||||
"lodash.merge": "^4.6.2",
|
"@cornerstonejs/core": "^0.13.11",
|
||||||
|
"@cornerstonejs/streaming-image-volume-loader": "^0.4.10",
|
||||||
|
"@cornerstonejs/tools": "^0.20.15",
|
||||||
|
"@kitware/vtk.js": "^24.18.7",
|
||||||
|
"dom-to-image": "^2.6.0",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"react-cornerstone-viewport": "4.1.2"
|
"lodash.merge": "^4.6.2",
|
||||||
|
"shader-loader": "^1.3.1",
|
||||||
|
"worker-loader": "^3.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,152 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { ViewportDownloadForm } from '@ohif/ui';
|
|
||||||
import { utils } from '@ohif/core';
|
|
||||||
|
|
||||||
import { getEnabledElement } from './state';
|
|
||||||
|
|
||||||
const MINIMUM_SIZE = 100;
|
|
||||||
const DEFAULT_SIZE = 512;
|
|
||||||
const MAX_TEXTURE_SIZE = 10000;
|
|
||||||
|
|
||||||
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
|
|
||||||
const enabledElement = getEnabledElement(
|
|
||||||
activeViewportIndex
|
|
||||||
);
|
|
||||||
const activeEnabledElement = enabledElement?.element
|
|
||||||
|
|
||||||
const enableViewport = viewportElement => {
|
|
||||||
if (viewportElement) {
|
|
||||||
cornerstone.enable(viewportElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableViewport = viewportElement => {
|
|
||||||
if (viewportElement) {
|
|
||||||
cornerstone.disable(viewportElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
|
|
||||||
new Promise(resolve => {
|
|
||||||
cornerstone.fitToWindow(viewportElement);
|
|
||||||
|
|
||||||
viewportElement.addEventListener(
|
|
||||||
'cornerstoneimagerendered',
|
|
||||||
function updateViewport(event) {
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(event.target)
|
|
||||||
.element;
|
|
||||||
const type = 'image/' + fileType;
|
|
||||||
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
|
||||||
|
|
||||||
let newWidth = enabledElement.offsetHeight;
|
|
||||||
let newHeight = enabledElement.offsetWidth;
|
|
||||||
|
|
||||||
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
|
||||||
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
|
||||||
newHeight *= multiplier;
|
|
||||||
newWidth *= multiplier;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
|
||||||
|
|
||||||
viewportElement.removeEventListener(
|
|
||||||
'cornerstoneimagerendered',
|
|
||||||
updateViewport
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadImage = (activeViewport, viewportElement, width, height) =>
|
|
||||||
new Promise(resolve => {
|
|
||||||
if (activeViewport && viewportElement) {
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(activeViewport);
|
|
||||||
const viewport = Object.assign({}, enabledElement.viewport);
|
|
||||||
delete viewport.scale;
|
|
||||||
viewport.translation = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
|
|
||||||
cornerstone.displayImage(viewportElement, image);
|
|
||||||
cornerstone.setViewport(viewportElement, viewport);
|
|
||||||
cornerstone.resize(viewportElement, true);
|
|
||||||
|
|
||||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
|
||||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
|
||||||
|
|
||||||
resolve({ image, width: newWidth, height: newHeight });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleAnnotations = (toggle, viewportElement) => {
|
|
||||||
cornerstoneTools.store.state.tools.forEach(({ name }) => {
|
|
||||||
if (toggle) {
|
|
||||||
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
|
|
||||||
} else {
|
|
||||||
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadBlob = (
|
|
||||||
filename,
|
|
||||||
fileType,
|
|
||||||
viewportElement,
|
|
||||||
downloadCanvas
|
|
||||||
) => {
|
|
||||||
const file = `${filename}.${fileType}`;
|
|
||||||
const mimetype = `image/${fileType}`;
|
|
||||||
|
|
||||||
/* Handles JPEG images for IE11 */
|
|
||||||
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
|
|
||||||
const image = downloadCanvas.toDataURL(mimetype, 1);
|
|
||||||
const blob = utils.b64toBlob(
|
|
||||||
image.replace('data:image/jpeg;base64,', ''),
|
|
||||||
mimetype
|
|
||||||
);
|
|
||||||
return window.navigator.msSaveBlob(blob, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewportElement.querySelector('canvas').toBlob(blob => {
|
|
||||||
const URLObj = window.URL || window.webkitURL;
|
|
||||||
const a = document.createElement('a');
|
|
||||||
|
|
||||||
a.href = URLObj.createObjectURL(blob);
|
|
||||||
a.download = file;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewportDownloadForm
|
|
||||||
onClose={onClose}
|
|
||||||
minimumSize={MINIMUM_SIZE}
|
|
||||||
maximumSize={MAX_TEXTURE_SIZE}
|
|
||||||
defaultSize={DEFAULT_SIZE}
|
|
||||||
canvasClass={'cornerstone-canvas'}
|
|
||||||
activeViewport={activeEnabledElement}
|
|
||||||
enableViewport={enableViewport}
|
|
||||||
disableViewport={disableViewport}
|
|
||||||
updateViewportPreview={updateViewportPreview}
|
|
||||||
loadImage={loadImage}
|
|
||||||
toggleAnnotations={toggleAnnotations}
|
|
||||||
downloadBlob={downloadBlob}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CornerstoneViewportDownloadForm.propTypes = {
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CornerstoneViewportDownloadForm;
|
|
||||||
@ -1,398 +0,0 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
|
||||||
|
|
||||||
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
|
||||||
import setCornerstoneMeasurementActive from './_shared/setCornerstoneMeasurementActive';
|
|
||||||
import setActiveAndPassiveToolsForElement from './_shared/setActiveAndPassiveToolsForElement';
|
|
||||||
import getTools from './_shared/getTools';
|
|
||||||
|
|
||||||
import ViewportOverlay from './ViewportOverlay';
|
|
||||||
|
|
||||||
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
|
||||||
|
|
||||||
const BaseAnnotationTool = cornerstoneTools.importInternal(
|
|
||||||
'base/BaseAnnotationTool'
|
|
||||||
);
|
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
|
||||||
|
|
||||||
function OHIFCornerstoneViewport({
|
|
||||||
children,
|
|
||||||
dataSource,
|
|
||||||
displaySet,
|
|
||||||
onElementEnabled,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
ToolBarService,
|
|
||||||
DisplaySetService,
|
|
||||||
MeasurementService,
|
|
||||||
HangingProtocolService,
|
|
||||||
} = servicesManager.services;
|
|
||||||
const [viewportData, setViewportData] = useState(null);
|
|
||||||
const [{ cines }, cineService] = useCine();
|
|
||||||
const [{ viewports }, viewportGridService] = useViewportGrid();
|
|
||||||
|
|
||||||
const isMounted = useRef(false);
|
|
||||||
const stageChangedRef = useRef(false);
|
|
||||||
|
|
||||||
const onNewImage = (element, callback) => {
|
|
||||||
const handler = () => {
|
|
||||||
element.removeEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
|
|
||||||
callback(element, ToolBarService);
|
|
||||||
};
|
|
||||||
element.addEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultOnElementEnabled = evt => {
|
|
||||||
const eventData = evt.detail;
|
|
||||||
const targetElement = eventData.element;
|
|
||||||
const tools = getTools();
|
|
||||||
const toolAlias = ToolBarService.state.primaryToolId;
|
|
||||||
|
|
||||||
// Activate appropriate tool bindings for element
|
|
||||||
setActiveAndPassiveToolsForElement(targetElement, tools);
|
|
||||||
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
|
|
||||||
mouseButtonMask: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set dashed, based on tracking, for this viewport
|
|
||||||
const allTools = cornerstoneTools.store.state.tools;
|
|
||||||
const toolsForElement = allTools.filter(
|
|
||||||
tool => tool.element === targetElement
|
|
||||||
);
|
|
||||||
|
|
||||||
toolsForElement.forEach(tool => {
|
|
||||||
if (tool instanceof BaseAnnotationTool) {
|
|
||||||
const configuration = tool.configuration;
|
|
||||||
|
|
||||||
configuration.renderDashed = true;
|
|
||||||
|
|
||||||
tool.configuration = configuration;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update image after setting tool config
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(targetElement);
|
|
||||||
|
|
||||||
if (enabledElement.image) {
|
|
||||||
cornerstone.updateImage(targetElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
isMounted.current = true;
|
|
||||||
return () => {
|
|
||||||
isMounted.current = false;
|
|
||||||
StackManager.clearStacks();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { unsubscribe } = HangingProtocolService.subscribe(
|
|
||||||
HangingProtocolService.EVENTS.STAGE_CHANGE,
|
|
||||||
() => {
|
|
||||||
stageChangedRef.current = true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cineService.setCine({ id: viewportIndex });
|
|
||||||
}, [viewportIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const {
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
sopClassUids,
|
|
||||||
} = displaySet;
|
|
||||||
|
|
||||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sopClassUids && sopClassUids.length > 1) {
|
|
||||||
console.warn(
|
|
||||||
'More than one SOPClassUID in the same series is not yet supported.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_getViewportData(dataSource, displaySet).then(data => {
|
|
||||||
if (isMounted.current) setViewportData(data);
|
|
||||||
});
|
|
||||||
}, [dataSource, displaySet, viewportIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
displaySet.displaySetInstanceUID,
|
|
||||||
viewportGridService
|
|
||||||
);
|
|
||||||
|
|
||||||
_checkForCachedJumpToMeasurementEvents(
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
displaySet.displaySetInstanceUID,
|
|
||||||
viewportGridService
|
|
||||||
);
|
|
||||||
|
|
||||||
// reseting the HP stage changed flag
|
|
||||||
if (element) {
|
|
||||||
onNewImage(element, () => {
|
|
||||||
stageChangedRef.current = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// running HP-defined callbacks: invert, window level ...
|
|
||||||
if (element && displaySet.renderedCallback) {
|
|
||||||
onNewImage(element, displaySet.renderedCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribeFromJumpToMeasurementEvents();
|
|
||||||
};
|
|
||||||
}, [element, displaySet]);
|
|
||||||
|
|
||||||
let childrenWithProps = null;
|
|
||||||
|
|
||||||
if (!viewportData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
imageIds,
|
|
||||||
initialImageIdIndex,
|
|
||||||
// If this comes from the instance, would be a better default
|
|
||||||
// `FrameTime` in the instance
|
|
||||||
// frameRate = 0,
|
|
||||||
} = viewportData.stack;
|
|
||||||
|
|
||||||
// TODO: Does it make more sense to use Context?
|
|
||||||
if (children && children.length) {
|
|
||||||
childrenWithProps = children.map((child, index) => {
|
|
||||||
return (
|
|
||||||
child &&
|
|
||||||
React.cloneElement(child, {
|
|
||||||
viewportIndex,
|
|
||||||
key: index,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have...
|
|
||||||
// StudyInstanceUid, DisplaySetInstanceUid
|
|
||||||
// Use displaySetInstanceUid --> SeriesInstanceUid
|
|
||||||
// Get meta for series, map to actionBar
|
|
||||||
// const displaySet = DisplaySetService.getDisplaySetByUID(
|
|
||||||
// dSet.displaySetInstanceUID
|
|
||||||
// );
|
|
||||||
// TODO: This display contains the meta for all instances.
|
|
||||||
// That can't be right...
|
|
||||||
// console.log('DISPLAYSET', displaySet);
|
|
||||||
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
|
|
||||||
// console.log(seriesMeta);
|
|
||||||
|
|
||||||
const cine = cines[viewportIndex];
|
|
||||||
const isPlaying = (cine && cine.isPlaying) || false;
|
|
||||||
const frameRate = (cine && cine.frameRate) || 24;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
|
||||||
<CornerstoneViewport
|
|
||||||
onElementEnabled={
|
|
||||||
onElementEnabled ? onElementEnabled : defaultOnElementEnabled
|
|
||||||
}
|
|
||||||
viewportIndex={viewportIndex}
|
|
||||||
imageIds={imageIds}
|
|
||||||
imageIdIndex={initialImageIdIndex}
|
|
||||||
initialViewport={displaySet.initialViewport} // from hanging protocol
|
|
||||||
stageChanged={stageChangedRef.current}
|
|
||||||
// Sync resize throttle w/ sidepanel animation duration to prevent
|
|
||||||
// seizure inducing strobe blinking effect
|
|
||||||
resizeRefreshRateMs={150}
|
|
||||||
// TODO: ViewportGrid Context?
|
|
||||||
isActive={true} // todo
|
|
||||||
isStackPrefetchEnabled={true} // todo
|
|
||||||
isPlaying={isPlaying}
|
|
||||||
frameRate={frameRate}
|
|
||||||
isOverlayVisible={true}
|
|
||||||
loadingIndicatorComponent={ViewportLoadingIndicator}
|
|
||||||
viewportOverlayComponent={props => {
|
|
||||||
return (
|
|
||||||
<ViewportOverlay
|
|
||||||
{...props}
|
|
||||||
activeTools={ToolBarService.getActiveTools()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{childrenWithProps}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OHIFCornerstoneViewport.propTypes = {
|
|
||||||
displaySet: PropTypes.object,
|
|
||||||
viewportIndex: PropTypes.number,
|
|
||||||
dataSource: PropTypes.object,
|
|
||||||
children: PropTypes.node,
|
|
||||||
customProps: PropTypes.object,
|
|
||||||
ToolBarService: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
OHIFCornerstoneViewport.defaultProps = {
|
|
||||||
customProps: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
|
||||||
|
|
||||||
function _getCornerstoneStack(displaySet, dataSource) {
|
|
||||||
// Get stack from Stack Manager
|
|
||||||
const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
|
|
||||||
|
|
||||||
// Clone the stack here so we don't mutate it
|
|
||||||
const stack = Object.assign({}, storedStack);
|
|
||||||
|
|
||||||
return stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _getViewportData(dataSource, displaySet) {
|
|
||||||
const stack = _getCornerstoneStack(displaySet, dataSource);
|
|
||||||
|
|
||||||
const viewportData = {
|
|
||||||
StudyInstanceUID: displaySet.StudyInstanceUID,
|
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
|
||||||
stack,
|
|
||||||
};
|
|
||||||
|
|
||||||
return viewportData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _subscribeToJumpToMeasurementEvents(
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
viewportGridService
|
|
||||||
) {
|
|
||||||
const { unsubscribe } = MeasurementService.subscribe(
|
|
||||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
|
|
||||||
({ measurement }) => {
|
|
||||||
if (!measurement) return;
|
|
||||||
// check if the correct viewport index.
|
|
||||||
// if (viewportIndex !== jumpToMeasurementViewportIndex) {
|
|
||||||
// // Event for a different viewport.
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Jump the the measurement if the displaySetInstanceUID matches
|
|
||||||
if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
|
|
||||||
_jumpToMeasurement(
|
|
||||||
measurement,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
viewportGridService
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _checkForCachedJumpToMeasurementEvents(
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
viewportGridService
|
|
||||||
) {
|
|
||||||
// Check if there is a queued jumpToMeasurement event
|
|
||||||
const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement(
|
|
||||||
viewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
if (measurementIdToJumpTo && element) {
|
|
||||||
// Jump to measurement if the measurement exists
|
|
||||||
const measurement = MeasurementService.getMeasurement(
|
|
||||||
measurementIdToJumpTo
|
|
||||||
);
|
|
||||||
|
|
||||||
if (measurement.displaySetInstanceUID === displaySetInstanceUID) {
|
|
||||||
_jumpToMeasurement(
|
|
||||||
measurement,
|
|
||||||
element,
|
|
||||||
viewportIndex,
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
viewportGridService
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _jumpToMeasurement(
|
|
||||||
measurement,
|
|
||||||
targetElement,
|
|
||||||
viewportIndex,
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService,
|
|
||||||
viewportGridService
|
|
||||||
) {
|
|
||||||
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
|
|
||||||
|
|
||||||
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
|
|
||||||
displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
const imageIndex = referencedDisplaySet.images.findIndex(
|
|
||||||
i => i.SOPInstanceUID === SOPInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
setCornerstoneMeasurementActive(measurement);
|
|
||||||
viewportGridService.setActiveViewportIndex(viewportIndex);
|
|
||||||
if (targetElement !== null) {
|
|
||||||
const enabledElement = cornerstone.getEnabledElement(targetElement);
|
|
||||||
|
|
||||||
// Wait for the image to update or we get a race condition when the element has only just been enabled.
|
|
||||||
const scrollToHandler = evt => {
|
|
||||||
scrollToIndex(targetElement, imageIndex);
|
|
||||||
targetElement.removeEventListener(
|
|
||||||
'cornerstoneimagerendered',
|
|
||||||
scrollToHandler
|
|
||||||
);
|
|
||||||
};
|
|
||||||
targetElement.addEventListener('cornerstoneimagerendered', scrollToHandler);
|
|
||||||
|
|
||||||
if (enabledElement.image) {
|
|
||||||
cornerstone.updateImage(targetElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Jump to measurement consumed, remove.
|
|
||||||
MeasurementService.removeJumpToMeasurement(viewportIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OHIFCornerstoneViewport;
|
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
.viewport-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* MUST have `height` to prevent resize infinite loop */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cornerstone-viewport-element {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
/* Prevent the blue outline in Chrome when a viewport is selected */
|
||||||
|
outline: 0 !important;
|
||||||
|
|
||||||
|
/* Prevents the entire page from getting larger
|
||||||
|
when the magnify tool is near the sides/corners of the page */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
432
extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
Normal file
432
extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
Normal file
@ -0,0 +1,432 @@
|
|||||||
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useViewportGrid } from '@ohif/ui';
|
||||||
|
import * as cs3DTools from '@cornerstonejs/tools';
|
||||||
|
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
import { setEnabledElement } from '../state';
|
||||||
|
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
|
||||||
|
|
||||||
|
import './OHIFCornerstoneViewport.css';
|
||||||
|
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||||
|
|
||||||
|
const STACK = 'stack';
|
||||||
|
|
||||||
|
function areEqual(prevProps, nextProps) {
|
||||||
|
if (nextProps.needsRerendering) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevDisplaySets = prevProps.displaySets[0];
|
||||||
|
const nextDisplaySets = nextProps.displaySets[0];
|
||||||
|
|
||||||
|
if (prevDisplaySets && nextDisplaySets) {
|
||||||
|
const areSameDisplaySetInstanceUIDs =
|
||||||
|
prevDisplaySets.displaySetInstanceUID ===
|
||||||
|
nextDisplaySets.displaySetInstanceUID;
|
||||||
|
const areSameImageLength =
|
||||||
|
prevDisplaySets.images.length === nextDisplaySets.images.length;
|
||||||
|
const areSameImageIds = prevDisplaySets.images.every(
|
||||||
|
(prevImage, index) =>
|
||||||
|
prevImage.imageId === nextDisplaySets.images[index].imageId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
areSameDisplaySetInstanceUIDs && areSameImageLength && areSameImageIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||||
|
// Then we don't need to worry about the re-renders if the props change.
|
||||||
|
const OHIFCornerstoneViewport = React.memo(props => {
|
||||||
|
const {
|
||||||
|
viewportIndex,
|
||||||
|
displaySets,
|
||||||
|
dataSource,
|
||||||
|
viewportOptions,
|
||||||
|
displaySetOptions,
|
||||||
|
servicesManager,
|
||||||
|
onElementEnabled,
|
||||||
|
onElementDisabled,
|
||||||
|
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||||
|
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||||
|
// to set the initial state of the viewport's first image to render
|
||||||
|
initialImageIndex,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
|
const [viewportData, setViewportData] = useState(null);
|
||||||
|
const [_, viewportGridService] = useViewportGrid();
|
||||||
|
|
||||||
|
const elementRef = useRef();
|
||||||
|
|
||||||
|
const {
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
ToolBarService,
|
||||||
|
ToolGroupService,
|
||||||
|
SyncGroupService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
// useCallback for scroll bar height calculation
|
||||||
|
const setImageScrollBarHeight = useCallback(() => {
|
||||||
|
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
||||||
|
setScrollbarHeight(scrollbarHeight);
|
||||||
|
}, [elementRef]);
|
||||||
|
|
||||||
|
// useCallback for onResize
|
||||||
|
const onResize = useCallback(() => {
|
||||||
|
if (elementRef.current) {
|
||||||
|
CornerstoneViewportService.resize();
|
||||||
|
setImageScrollBarHeight();
|
||||||
|
}
|
||||||
|
}, [elementRef]);
|
||||||
|
|
||||||
|
const elementEnabledHandler = useCallback(
|
||||||
|
evt => {
|
||||||
|
// check this is this element reference and return early if doesn't match
|
||||||
|
if (evt.detail.element !== elementRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewportId, element } = evt.detail;
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfo(
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
const viewportIndex = viewportInfo.getViewportIndex();
|
||||||
|
|
||||||
|
setEnabledElement(viewportIndex, element);
|
||||||
|
|
||||||
|
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||||
|
const toolGroupId = viewportInfo.getToolGroupId();
|
||||||
|
const syncGroups = viewportInfo.getSyncGroups();
|
||||||
|
|
||||||
|
ToolGroupService.addViewportToToolGroup(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
|
||||||
|
SyncGroupService.addViewportToSyncGroup(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
syncGroups
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onElementEnabled) {
|
||||||
|
onElementEnabled(evt);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[viewportIndex, onElementEnabled, ToolGroupService]
|
||||||
|
);
|
||||||
|
|
||||||
|
// disable the element upon unmounting
|
||||||
|
useEffect(() => {
|
||||||
|
CornerstoneViewportService.enableElement(
|
||||||
|
viewportIndex,
|
||||||
|
viewportOptions,
|
||||||
|
elementRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
Enums.Events.ELEMENT_ENABLED,
|
||||||
|
elementEnabledHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
setImageScrollBarHeight();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewportId = viewportInfo.getViewportId();
|
||||||
|
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||||
|
const syncGroups = viewportInfo.getSyncGroups();
|
||||||
|
|
||||||
|
ToolGroupService.disable(viewportId, renderingEngineId);
|
||||||
|
SyncGroupService.removeViewportFromSyncGroup(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
syncGroups
|
||||||
|
);
|
||||||
|
|
||||||
|
CornerstoneViewportService.disableElement(viewportIndex);
|
||||||
|
|
||||||
|
eventTarget.removeEventListener(
|
||||||
|
Enums.Events.ELEMENT_ENABLED,
|
||||||
|
elementEnabledHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onElementDisabled) {
|
||||||
|
onElementDisabled();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// subscribe to displaySet metadata invalidation (updates)
|
||||||
|
// Currently, if the metadata changes we need to re-render the display set
|
||||||
|
// for it to take effect in the viewport. As we deal with scaling in the loading,
|
||||||
|
// we need to remove the old volume from the cache, and let the
|
||||||
|
// viewport to re-add it which will use the new metadata. Otherwise, the
|
||||||
|
// viewport will use the cached volume and the new metadata will not be used.
|
||||||
|
// Note: this approach does not actually end of sending network requests
|
||||||
|
// and it uses the network cache
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = DisplaySetService.subscribe(
|
||||||
|
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||||
|
async invalidatedDisplaySetInstanceUID => {
|
||||||
|
if (
|
||||||
|
viewportData.displaySetInstanceUIDs.includes(
|
||||||
|
invalidatedDisplaySetInstanceUID
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const newViewportData = await CornerstoneCacheService.invalidateViewportData(
|
||||||
|
viewportData,
|
||||||
|
invalidatedDisplaySetInstanceUID,
|
||||||
|
dataSource,
|
||||||
|
DisplaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
CornerstoneViewportService.updateViewport(
|
||||||
|
viewportIndex,
|
||||||
|
newViewportData
|
||||||
|
);
|
||||||
|
|
||||||
|
setViewportData(newViewportData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [viewportData, viewportIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// handle the default viewportType to be stack
|
||||||
|
if (!viewportOptions.viewportType) {
|
||||||
|
viewportOptions.viewportType = STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadViewportData = async () => {
|
||||||
|
const viewportData = await CornerstoneCacheService.getViewportData(
|
||||||
|
viewportIndex,
|
||||||
|
displaySets,
|
||||||
|
viewportOptions.viewportType,
|
||||||
|
dataSource,
|
||||||
|
initialImageIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
CornerstoneViewportService.setViewportDisplaySets(
|
||||||
|
viewportIndex,
|
||||||
|
viewportData,
|
||||||
|
viewportOptions,
|
||||||
|
displaySetOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
setViewportData(viewportData);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadViewportData();
|
||||||
|
}, [viewportOptions, displaySets, dataSource]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There are two scenarios for jump to click
|
||||||
|
* 1. Current viewports contain the displaySet that the annotation was drawn on
|
||||||
|
* 2. Current viewports don't contain the displaySet that the annotation was drawn on
|
||||||
|
* and we need to change the viewports displaySet for jumping.
|
||||||
|
* Since measurement_jump happens via events and listeners, the former case is handled
|
||||||
|
* by the measurement_jump direct callback, but the latter case is handled first by
|
||||||
|
* the viewportGrid to set the correct displaySet on the viewport, AND THEN we check
|
||||||
|
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
elementRef,
|
||||||
|
viewportIndex,
|
||||||
|
displaySets,
|
||||||
|
viewportGridService
|
||||||
|
);
|
||||||
|
|
||||||
|
_checkForCachedJumpToMeasurementEvents(
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
elementRef,
|
||||||
|
viewportIndex,
|
||||||
|
displaySets,
|
||||||
|
viewportGridService
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribeFromJumpToMeasurementEvents();
|
||||||
|
};
|
||||||
|
}, [displaySets, elementRef, viewportIndex, viewportData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="viewport-wrapper">
|
||||||
|
<ReactResizeDetector
|
||||||
|
handleWidth
|
||||||
|
handleHeight
|
||||||
|
skipOnMount={true} // Todo: make these configurable
|
||||||
|
refreshMode={'debounce'}
|
||||||
|
refreshRate={100}
|
||||||
|
onResize={onResize}
|
||||||
|
targetRef={elementRef.current}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="cornerstone-viewport-element"
|
||||||
|
style={{ height: '100%', width: '100%' }}
|
||||||
|
onContextMenu={e => e.preventDefault()}
|
||||||
|
onMouseDown={e => e.preventDefault()}
|
||||||
|
ref={elementRef}
|
||||||
|
></div>
|
||||||
|
<CornerstoneOverlays
|
||||||
|
viewportIndex={viewportIndex}
|
||||||
|
ToolBarService={ToolBarService}
|
||||||
|
element={elementRef.current}
|
||||||
|
scrollbarHeight={scrollbarHeight}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, areEqual);
|
||||||
|
|
||||||
|
function _subscribeToJumpToMeasurementEvents(
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
elementRef,
|
||||||
|
viewportIndex,
|
||||||
|
displaySets,
|
||||||
|
viewportGridService
|
||||||
|
) {
|
||||||
|
const displaysUIDs = displaySets.map(
|
||||||
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
const { unsubscribe } = MeasurementService.subscribe(
|
||||||
|
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT,
|
||||||
|
({ measurement }) => {
|
||||||
|
if (!measurement) return;
|
||||||
|
|
||||||
|
// Jump the the measurement if the viewport contains the displaySetUID (fusion)
|
||||||
|
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
||||||
|
_jumpToMeasurement(
|
||||||
|
measurement,
|
||||||
|
elementRef,
|
||||||
|
viewportIndex,
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
viewportGridService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is a queued jumpToMeasurement event
|
||||||
|
function _checkForCachedJumpToMeasurementEvents(
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
elementRef,
|
||||||
|
viewportIndex,
|
||||||
|
displaySets,
|
||||||
|
viewportGridService
|
||||||
|
) {
|
||||||
|
const displaysUIDs = displaySets.map(
|
||||||
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const measurementIdToJumpTo = MeasurementService.getJumpToMeasurement(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (measurementIdToJumpTo && elementRef) {
|
||||||
|
// Jump to measurement if the measurement exists
|
||||||
|
const measurement = MeasurementService.getMeasurement(
|
||||||
|
measurementIdToJumpTo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
|
||||||
|
_jumpToMeasurement(
|
||||||
|
measurement,
|
||||||
|
elementRef,
|
||||||
|
viewportIndex,
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
viewportGridService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _jumpToMeasurement(
|
||||||
|
measurement,
|
||||||
|
targetElementRef,
|
||||||
|
viewportIndex,
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
viewportGridService
|
||||||
|
) {
|
||||||
|
const targetElement = targetElementRef.current;
|
||||||
|
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
|
||||||
|
|
||||||
|
if (!SOPInstanceUID) {
|
||||||
|
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = DisplaySetService.getDisplaySetByUID(
|
||||||
|
displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||||
|
i => i.SOPInstanceUID === SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
||||||
|
// to set it properly
|
||||||
|
// setCornerstoneMeasurementActive(measurement);
|
||||||
|
|
||||||
|
viewportGridService.setActiveViewportIndex(viewportIndex);
|
||||||
|
|
||||||
|
if (getEnabledElement(targetElement)) {
|
||||||
|
cs3DTools.utilities.jumpToSlice(targetElement, {
|
||||||
|
imageIndex: imageIdIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||||
|
// Jump to measurement consumed, remove.
|
||||||
|
MeasurementService.removeJumpToMeasurement(viewportIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component displayName
|
||||||
|
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||||
|
|
||||||
|
OHIFCornerstoneViewport.propTypes = {
|
||||||
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
|
displaySets: PropTypes.array.isRequired,
|
||||||
|
dataSource: PropTypes.object.isRequired,
|
||||||
|
viewportOptions: PropTypes.object,
|
||||||
|
displaySetOptions: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
servicesManager: PropTypes.object.isRequired,
|
||||||
|
onElementEnabled: PropTypes.func,
|
||||||
|
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||||
|
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||||
|
// to set the initial state of the viewport's first image to render
|
||||||
|
initialImageIdOrIndex: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OHIFCornerstoneViewport;
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import ViewportImageScrollbar from './ViewportImageScrollbar';
|
||||||
|
import ViewportOverlay from './ViewportOverlay';
|
||||||
|
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||||
|
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
||||||
|
import CornerstoneCacheService from '../../services/ViewportService/CornerstoneCacheService';
|
||||||
|
|
||||||
|
function CornerstoneOverlays(props) {
|
||||||
|
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
|
||||||
|
const { CornerstoneViewportService } = servicesManager.services;
|
||||||
|
const [imageSliceData, setImageSliceData] = useState({
|
||||||
|
imageIndex: 0,
|
||||||
|
numberOfSlices: 0,
|
||||||
|
});
|
||||||
|
const [viewportData, setViewportData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = CornerstoneCacheService.subscribe(
|
||||||
|
CornerstoneCacheService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||||
|
props => {
|
||||||
|
if (props.viewportIndex !== viewportIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewportData(props.viewportData);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [viewportIndex]);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportData) {
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (viewportInfo?.viewportOptions?.customViewportOptions?.hideOverlays) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ViewportImageScrollbar
|
||||||
|
viewportIndex={viewportIndex}
|
||||||
|
viewportData={viewportData}
|
||||||
|
element={element}
|
||||||
|
imageSliceData={imageSliceData}
|
||||||
|
setImageSliceData={setImageSliceData}
|
||||||
|
scrollbarHeight={scrollbarHeight}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
/>
|
||||||
|
<ViewportOverlay
|
||||||
|
imageSliceData={imageSliceData}
|
||||||
|
viewportData={viewportData}
|
||||||
|
viewportIndex={viewportIndex}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
element={element}
|
||||||
|
/>
|
||||||
|
<ViewportLoadingIndicator viewportData={viewportData} element={element} />
|
||||||
|
<ViewportOrientationMarkers
|
||||||
|
imageSliceData={imageSliceData}
|
||||||
|
element={element}
|
||||||
|
viewportData={viewportData}
|
||||||
|
viewportIndex={viewportIndex}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CornerstoneOverlays;
|
||||||
@ -0,0 +1,145 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Enums, Types, utilities } from '@cornerstonejs/core';
|
||||||
|
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||||
|
import { ImageScrollbar } from '@ohif/ui';
|
||||||
|
|
||||||
|
function CornerstoneImageScrollbar({
|
||||||
|
viewportData,
|
||||||
|
viewportIndex,
|
||||||
|
element,
|
||||||
|
imageSliceData,
|
||||||
|
setImageSliceData,
|
||||||
|
scrollbarHeight,
|
||||||
|
servicesManager,
|
||||||
|
}) {
|
||||||
|
const { CineService, CornerstoneViewportService } = servicesManager.services;
|
||||||
|
|
||||||
|
const onImageScrollbarChange = (imageIndex, viewportIndex) => {
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewportId = viewportInfo.getViewportId();
|
||||||
|
const viewport = CornerstoneViewportService.getCornerstoneViewport(
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
// on image scrollbar change, stop the CINE if it is playing
|
||||||
|
CineService.stopClip(element);
|
||||||
|
CineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||||
|
|
||||||
|
csToolsUtils.jumpToSlice(viewport.element, {
|
||||||
|
imageIndex,
|
||||||
|
debounceLoading: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewportData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
|
const imageIndex = viewport.getCurrentImageIdIndex();
|
||||||
|
|
||||||
|
setImageSliceData({
|
||||||
|
imageIndex: imageIndex,
|
||||||
|
numberOfSlices: viewportData.imageIds.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
const sliceData = utilities.getImageSliceDataForVolumeViewport(
|
||||||
|
viewport as Types.IVolumeViewport
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sliceData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { imageIndex, numberOfSlices } = sliceData;
|
||||||
|
setImageSliceData({ imageIndex, numberOfSlices });
|
||||||
|
}
|
||||||
|
}, [viewportIndex, viewportData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStackIndex = event => {
|
||||||
|
const { newImageIdIndex } = event.detail;
|
||||||
|
// find the index of imageId in the imageIds
|
||||||
|
setImageSliceData({
|
||||||
|
imageIndex: newImageIdIndex,
|
||||||
|
numberOfSlices: viewportData.imageIds.length,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(
|
||||||
|
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||||
|
updateStackIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(
|
||||||
|
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||||
|
updateStackIndex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [viewportData, element]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewportData?.viewportType !== Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVolumeIndex = event => {
|
||||||
|
const { imageIndex, numberOfSlices } = event.detail;
|
||||||
|
// find the index of imageId in the imageIds
|
||||||
|
setImageSliceData({ imageIndex, numberOfSlices });
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(
|
||||||
|
Enums.Events.VOLUME_NEW_IMAGE,
|
||||||
|
updateVolumeIndex
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [viewportData, element]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImageScrollbar
|
||||||
|
onChange={evt => onImageScrollbarChange(evt, viewportIndex)}
|
||||||
|
max={
|
||||||
|
imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0
|
||||||
|
}
|
||||||
|
height={scrollbarHeight}
|
||||||
|
value={imageSliceData.imageIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CornerstoneImageScrollbar.propTypes = {
|
||||||
|
viewportData: PropTypes.object,
|
||||||
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
|
element: PropTypes.instanceOf(Element),
|
||||||
|
scrollbarHeight: PropTypes.string,
|
||||||
|
imageSliceData: PropTypes.object.isRequired,
|
||||||
|
setImageSliceData: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CornerstoneImageScrollbar;
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
function ViewportLoadingIndicator({ viewportData, element }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
|
const loadIndicatorRef = useRef(null);
|
||||||
|
const imageIdToBeLoaded = useRef(null);
|
||||||
|
|
||||||
|
const setLoadingState = evt => {
|
||||||
|
clearTimeout(loadIndicatorRef.current);
|
||||||
|
|
||||||
|
loadIndicatorRef.current = setTimeout(() => {
|
||||||
|
setLoading(true);
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFinishLoadingState = evt => {
|
||||||
|
clearTimeout(loadIndicatorRef.current);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setErrorState = evt => {
|
||||||
|
clearTimeout(loadIndicatorRef.current);
|
||||||
|
|
||||||
|
if (imageIdToBeLoaded.current === evt.detail.imageId) {
|
||||||
|
setError(evt.detail.error);
|
||||||
|
imageIdToBeLoaded.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
element.addEventListener(
|
||||||
|
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||||
|
setLoadingState
|
||||||
|
);
|
||||||
|
element.addEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
|
||||||
|
element.addEventListener(
|
||||||
|
Enums.Events.STACK_NEW_IMAGE,
|
||||||
|
setFinishLoadingState
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(
|
||||||
|
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||||
|
setLoadingState
|
||||||
|
);
|
||||||
|
|
||||||
|
element.removeEventListener(
|
||||||
|
Enums.Events.STACK_NEW_IMAGE,
|
||||||
|
setFinishLoadingState
|
||||||
|
);
|
||||||
|
|
||||||
|
element.removeEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
|
||||||
|
};
|
||||||
|
}, [element, viewportData]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-black opacity-50 absolute h-full w-full top-0 left-0">
|
||||||
|
<div className="flex transparent items-center justify-center w-full h-full">
|
||||||
|
<p className="text-primary-light text-xl font-light">
|
||||||
|
<h4>Error Loading Image</h4>
|
||||||
|
<p>An error has occurred.</p>
|
||||||
|
<p>{error}</p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
// IMPORTANT: we need to use the pointer-events-none class to prevent the loading indicator from
|
||||||
|
// interacting with the mouse, since scrolling should propagate to the viewport underneath
|
||||||
|
<div className="pointer-events-none bg-black opacity-50 absolute h-full w-full top-0 left-0">
|
||||||
|
<div className="flex transparent items-center justify-center w-full h-full">
|
||||||
|
<p className="text-primary-light text-xl font-light">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewportLoadingIndicator.propTypes = {
|
||||||
|
percentComplete: PropTypes.number,
|
||||||
|
error: PropTypes.object,
|
||||||
|
element: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportLoadingIndicator.defaultProps = {
|
||||||
|
percentComplete: 0,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportLoadingIndicator;
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
.ViewportOrientationMarkers {
|
||||||
|
--marker-width: 100px;
|
||||||
|
--marker-height: 100px;
|
||||||
|
--scrollbar-width: 20px;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .orientation-marker {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .top-mid {
|
||||||
|
top: 5px;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .left-mid {
|
||||||
|
top: 47%;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .right-mid {
|
||||||
|
top: 47%;
|
||||||
|
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .bottom-mid {
|
||||||
|
top: calc(100% - var(--marker-height) - 5px);
|
||||||
|
left: 47%;
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-width: var(--marker-width);
|
||||||
|
}
|
||||||
|
.ViewportOrientationMarkers .bottom-mid .orientation-marker-value {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: var(--marker-height);
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { metaData, Enums, Types } from '@cornerstonejs/core';
|
||||||
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
import './ViewportOrientationMarkers.css';
|
||||||
|
import { getEnabledElement } from '../../state';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Computes the orientation labels on a Cornerstone-enabled Viewport element
|
||||||
|
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
|
||||||
|
*
|
||||||
|
* @param {*} rowCosines
|
||||||
|
* @param {*} columnCosines
|
||||||
|
* @param {*} rotation in degrees
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getOrientationMarkers(
|
||||||
|
rowCosines,
|
||||||
|
columnCosines,
|
||||||
|
rotation,
|
||||||
|
flipVertical,
|
||||||
|
flipHorizontal
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
getOrientationStringLPS,
|
||||||
|
invertOrientationStringLPS,
|
||||||
|
} = utilities.orientation;
|
||||||
|
const rowString = getOrientationStringLPS(rowCosines);
|
||||||
|
const columnString = getOrientationStringLPS(columnCosines);
|
||||||
|
const oppositeRowString = invertOrientationStringLPS(rowString);
|
||||||
|
const oppositeColumnString = invertOrientationStringLPS(columnString);
|
||||||
|
|
||||||
|
const markers = {
|
||||||
|
top: oppositeColumnString,
|
||||||
|
left: oppositeRowString,
|
||||||
|
right: rowString,
|
||||||
|
bottom: columnString,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||||
|
// the rotation applications
|
||||||
|
if (flipVertical) {
|
||||||
|
markers.top = invertOrientationStringLPS(markers.top);
|
||||||
|
markers.bottom = invertOrientationStringLPS(markers.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flipHorizontal) {
|
||||||
|
markers.left = invertOrientationStringLPS(markers.left);
|
||||||
|
markers.right = invertOrientationStringLPS(markers.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the labels accordingly if the viewport has been rotated
|
||||||
|
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
|
||||||
|
if (rotation === 90 || rotation === -270) {
|
||||||
|
return {
|
||||||
|
top: markers.left,
|
||||||
|
left: invertOrientationStringLPS(markers.top),
|
||||||
|
right: invertOrientationStringLPS(markers.bottom),
|
||||||
|
bottom: markers.right, // left
|
||||||
|
};
|
||||||
|
} else if (rotation === -90 || rotation === 270) {
|
||||||
|
return {
|
||||||
|
top: invertOrientationStringLPS(markers.left),
|
||||||
|
left: markers.top,
|
||||||
|
bottom: markers.left,
|
||||||
|
right: markers.bottom,
|
||||||
|
};
|
||||||
|
} else if (rotation === 180 || rotation === -180) {
|
||||||
|
return {
|
||||||
|
top: invertOrientationStringLPS(markers.top),
|
||||||
|
left: invertOrientationStringLPS(markers.left),
|
||||||
|
bottom: invertOrientationStringLPS(markers.bottom),
|
||||||
|
right: invertOrientationStringLPS(markers.right),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewportOrientationMarkers({
|
||||||
|
element,
|
||||||
|
viewportData,
|
||||||
|
imageSliceData,
|
||||||
|
viewportIndex,
|
||||||
|
orientationMarkers = ['top', 'left'],
|
||||||
|
}) {
|
||||||
|
// Rotation is in degrees
|
||||||
|
const [rotation, setRotation] = useState(0);
|
||||||
|
const [flipHorizontal, setFlipHorizontal] = useState(false);
|
||||||
|
const [flipVertical, setFlipVertical] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cameraModifiedListener = (
|
||||||
|
evt: Types.EventTypes.CameraModifiedEvent
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const { rotation, previousCamera, camera } = evt.detail;
|
||||||
|
|
||||||
|
if (rotation !== undefined) {
|
||||||
|
setRotation(rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera.flipHorizontal !== undefined &&
|
||||||
|
previousCamera.flipHorizontal !== camera.flipHorizontal) {
|
||||||
|
setFlipHorizontal(camera.flipHorizontal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera.flipVertical !== undefined &&
|
||||||
|
previousCamera.flipVertical !== camera.flipVertical) {
|
||||||
|
setFlipVertical(camera.flipVertical);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(
|
||||||
|
Enums.Events.CAMERA_MODIFIED,
|
||||||
|
cameraModifiedListener
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(
|
||||||
|
Enums.Events.CAMERA_MODIFIED,
|
||||||
|
cameraModifiedListener
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getMarkers = useCallback(
|
||||||
|
orientationMarkers => {
|
||||||
|
// Todo: support orientation markers for the volume viewports
|
||||||
|
if (
|
||||||
|
!viewportData ||
|
||||||
|
viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIndex = imageSliceData.imageIndex;
|
||||||
|
const imageId = viewportData?.imageIds[imageIndex];
|
||||||
|
|
||||||
|
// Workaround for below TODO stub
|
||||||
|
if (!imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rowCosines, columnCosines } =
|
||||||
|
metaData.get('imagePlaneModule', imageId) || {};
|
||||||
|
|
||||||
|
if (!rowCosines || !columnCosines || rotation === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rowCosines || !columnCosines) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const markers = getOrientationMarkers(
|
||||||
|
rowCosines,
|
||||||
|
columnCosines,
|
||||||
|
rotation,
|
||||||
|
flipVertical,
|
||||||
|
flipHorizontal
|
||||||
|
);
|
||||||
|
|
||||||
|
return orientationMarkers.map((m, index) => (
|
||||||
|
<div
|
||||||
|
className={`${m}-mid orientation-marker`}
|
||||||
|
key={`${m}-mid orientation-marker`}
|
||||||
|
>
|
||||||
|
<div className="orientation-marker-value">{markers[m]}</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
},
|
||||||
|
[flipHorizontal, flipVertical, rotation, viewportData, imageSliceData]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ViewportOrientationMarkers noselect">
|
||||||
|
{getMarkers(orientationMarkers)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewportOrientationMarkers.propTypes = {
|
||||||
|
percentComplete: PropTypes.number,
|
||||||
|
error: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportOrientationMarkers.defaultProps = {
|
||||||
|
percentComplete: 0,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportOrientationMarkers;
|
||||||
273
extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx
Normal file
273
extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||||
|
import { ViewportOverlay } from '@ohif/ui';
|
||||||
|
|
||||||
|
const EPSILON = 1e-4;
|
||||||
|
|
||||||
|
function CornerstoneViewportOverlay({
|
||||||
|
element,
|
||||||
|
viewportData,
|
||||||
|
imageSliceData,
|
||||||
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
CornerstoneViewportService,
|
||||||
|
ToolBarService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [activeTools, setActiveTools] = useState([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial toolbar state
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTools(ToolBarService.getActiveTools());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating the VOI when the viewport changes its voi
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const updateVOI = eventDetail => {
|
||||||
|
const { range } = eventDetail.detail;
|
||||||
|
|
||||||
|
if (!range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lower, upper } = range;
|
||||||
|
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
|
||||||
|
lower,
|
||||||
|
upper
|
||||||
|
);
|
||||||
|
|
||||||
|
setVOI({ windowCenter, windowWidth });
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||||
|
};
|
||||||
|
}, [viewportIndex, viewportData, voi, element]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating the scale when the viewport changes its zoom
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const updateScale = eventDetail => {
|
||||||
|
const { previousCamera, camera } = eventDetail.detail;
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousCamera.parallelScale !== camera.parallelScale ||
|
||||||
|
previousCamera.scale !== camera.scale
|
||||||
|
) {
|
||||||
|
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = viewport.getImageData();
|
||||||
|
|
||||||
|
if (!imageData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (camera.scale) {
|
||||||
|
setScale(camera.scale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { spacing } = imageData;
|
||||||
|
// convert parallel scale to scale
|
||||||
|
const scale =
|
||||||
|
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||||
|
setScale(scale);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||||
|
};
|
||||||
|
}, [viewportIndex, viewportData]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating the active tools when the toolbar changes
|
||||||
|
*/
|
||||||
|
// Todo: this should act on the toolGroups instead of the toolbar state
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = ToolBarService.subscribe(
|
||||||
|
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||||
|
() => {
|
||||||
|
setActiveTools(ToolBarService.getActiveTools());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [ToolBarService]);
|
||||||
|
|
||||||
|
const getTopLeftContent = useCallback(() => {
|
||||||
|
const { windowWidth, windowCenter } = voi;
|
||||||
|
|
||||||
|
if (activeTools.includes('WindowLevel')) {
|
||||||
|
if (typeof windowCenter !== 'number' || typeof windowWidth !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-1">W:</span>
|
||||||
|
<span className="ml-1 mr-2 font-light">{windowWidth.toFixed(0)}</span>
|
||||||
|
<span className="mr-1">L:</span>
|
||||||
|
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeTools.includes('Zoom')) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-1">Zoom:</span>
|
||||||
|
<span className="font-light">{scale.toFixed(2)}x</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [voi, scale, activeTools]);
|
||||||
|
|
||||||
|
const getTopRightContent = useCallback(() => {
|
||||||
|
const { imageIndex, numberOfSlices } = imageSliceData;
|
||||||
|
if (!viewportData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let instanceNumber;
|
||||||
|
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
|
instanceNumber = _getInstanceNumberFromStack(viewportData, imageIndex);
|
||||||
|
|
||||||
|
if (!instanceNumber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
instanceNumber = _getInstanceNumberFromVolume(
|
||||||
|
viewportData,
|
||||||
|
imageIndex,
|
||||||
|
viewportIndex,
|
||||||
|
CornerstoneViewportService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-1">I:</span>
|
||||||
|
<span className="font-light">
|
||||||
|
{instanceNumber !== undefined
|
||||||
|
? `${instanceNumber} (${imageIndex + 1}/${numberOfSlices})`
|
||||||
|
: `${imageIndex + 1}/${numberOfSlices}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [imageSliceData, viewportData, viewportIndex]);
|
||||||
|
|
||||||
|
if (!viewportData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportData.imageIds.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
'ViewportOverlay: only viewports with imageIds is supported at this time'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewportOverlay
|
||||||
|
topLeft={getTopLeftContent()}
|
||||||
|
topRight={getTopRightContent()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||||
|
const imageIds = viewportData.imageIds;
|
||||||
|
const imageId = imageIds[imageIndex];
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
|
||||||
|
const { instanceNumber } = generalImageModule;
|
||||||
|
|
||||||
|
const stackSize = imageIds.length;
|
||||||
|
|
||||||
|
if (stackSize <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseInt(instanceNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since volume viewports can be in any view direction, they can render
|
||||||
|
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||||
|
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||||
|
function _getInstanceNumberFromVolume(
|
||||||
|
viewportData,
|
||||||
|
imageIndex,
|
||||||
|
viewportIndex,
|
||||||
|
CornerstoneViewportService
|
||||||
|
) {
|
||||||
|
const volumes = viewportData.volumes;
|
||||||
|
|
||||||
|
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||||
|
if (!volumes || volumes.length > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volume = volumes[0];
|
||||||
|
const { direction, imageIds } = volume;
|
||||||
|
|
||||||
|
const cornerstoneViewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cornerstoneViewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const camera = cornerstoneViewport.getCamera();
|
||||||
|
const { viewPlaneNormal } = camera;
|
||||||
|
// checking if camera is looking at the acquisition plane (defined by the direction on the volume)
|
||||||
|
|
||||||
|
const scanAxisNormal = direction.slice(6, 9);
|
||||||
|
|
||||||
|
// check if viewPlaneNormal is parallel to scanAxisNormal
|
||||||
|
const cross = vec3.cross(vec3.create(), viewPlaneNormal, scanAxisNormal);
|
||||||
|
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
||||||
|
|
||||||
|
if (isAcquisitionPlane) {
|
||||||
|
const { instanceNumber } =
|
||||||
|
metaData.get('generalImageModule', imageIds[imageIndex]) || {};
|
||||||
|
return parseInt(instanceNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CornerstoneViewportOverlay.propTypes = {
|
||||||
|
viewportData: PropTypes.object,
|
||||||
|
imageIndex: PropTypes.number,
|
||||||
|
viewportIndex: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CornerstoneViewportOverlay;
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const ViewportLoadingIndicator = ({ error }) => {
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="bg-black h-full w-full absolute opacity-50"></div>
|
|
||||||
<div className="text-primary-light text-xl font-light">
|
|
||||||
<h4>Error Loading Image</h4>
|
|
||||||
<p>An error has occurred.</p>
|
|
||||||
<p>{error.message}</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="bg-black h-full w-full absolute opacity-50"></div>
|
|
||||||
<div className="absolute transparent w-full h-full flex items-center justify-center">
|
|
||||||
<p className="text-primary-light text-xl font-light">Loading...</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewportLoadingIndicator.propTypes = {
|
|
||||||
percentComplete: PropTypes.number,
|
|
||||||
error: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewportLoadingIndicator.defaultProps = {
|
|
||||||
percentComplete: 0,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewportLoadingIndicator;
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
const ViewportOverlay = ({
|
|
||||||
imageId,
|
|
||||||
scale,
|
|
||||||
windowWidth,
|
|
||||||
windowCenter,
|
|
||||||
imageIndex,
|
|
||||||
stackSize,
|
|
||||||
activeTools,
|
|
||||||
}) => {
|
|
||||||
const topLeft = 'top-viewport left-viewport';
|
|
||||||
const topRight = 'top-viewport right-viewport-scrollbar';
|
|
||||||
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
|
|
||||||
const bottomLeft = 'bottom-viewport left-viewport';
|
|
||||||
const overlay = 'absolute pointer-events-none';
|
|
||||||
|
|
||||||
const isZoomActive = activeTools.includes('Zoom');
|
|
||||||
const isWwwcActive = activeTools.includes('Wwwc');
|
|
||||||
|
|
||||||
if (!imageId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this component should be presentational only. Right now it has a weird dependency on Cornerstone
|
|
||||||
const generalImageModule =
|
|
||||||
cornerstone.metaData.get('generalImageModule', imageId) || {};
|
|
||||||
const { instanceNumber } = generalImageModule;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-primary-light">
|
|
||||||
<div
|
|
||||||
data-cy={'viewport-overlay-top-left'}
|
|
||||||
className={classnames(overlay, topLeft)}
|
|
||||||
>
|
|
||||||
{isZoomActive && (
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<span className="mr-1">Zoom:</span>
|
|
||||||
<span className="font-light">{scale.toFixed(2)}x</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isWwwcActive && (
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<span className="mr-1">W:</span>
|
|
||||||
<span className="ml-1 mr-2 font-light">
|
|
||||||
{windowWidth.toFixed(0)}
|
|
||||||
</span>
|
|
||||||
<span className="mr-1">L:</span>
|
|
||||||
<span className="ml-1 font-light">{windowCenter.toFixed(0)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-cy={'viewport-overlay-top-right'}
|
|
||||||
className={classnames(overlay, topRight)}
|
|
||||||
>
|
|
||||||
{stackSize > 1 && (
|
|
||||||
<div className="flex flex-row">
|
|
||||||
<span className="mr-1">I:</span>
|
|
||||||
<span className="font-light">
|
|
||||||
{`${instanceNumber} (${imageIndex}/${stackSize})`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
data-cy={'viewport-overlay-bottom-right'}
|
|
||||||
className={classnames(overlay, bottomRight)}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
data-cy={'viewport-overlay-bottom-left'}
|
|
||||||
className={classnames(overlay, bottomLeft)}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewportOverlay.propTypes = {
|
|
||||||
scale: PropTypes.number.isRequired,
|
|
||||||
windowWidth: PropTypes.number.isRequired,
|
|
||||||
windowCenter: PropTypes.number.isRequired,
|
|
||||||
imageId: PropTypes.string.isRequired,
|
|
||||||
imageIndex: PropTypes.number.isRequired,
|
|
||||||
stackSize: PropTypes.number.isRequired,
|
|
||||||
activeTools: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
};
|
|
||||||
|
|
||||||
ViewportOverlay.defaultProps = {
|
|
||||||
activeTools: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewportOverlay;
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import csTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
const toolsGroupedByType = {
|
|
||||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
|
||||||
annotations: [
|
|
||||||
csTools.ArrowAnnotateTool,
|
|
||||||
csTools.BidirectionalTool,
|
|
||||||
csTools.LengthTool,
|
|
||||||
csTools.AngleTool,
|
|
||||||
csTools.FreehandRoiTool,
|
|
||||||
csTools.EllipticalRoiTool,
|
|
||||||
csTools.DragProbeTool,
|
|
||||||
csTools.RectangleRoiTool,
|
|
||||||
],
|
|
||||||
other: [
|
|
||||||
csTools.PanTool,
|
|
||||||
csTools.ZoomTool,
|
|
||||||
csTools.WwwcTool,
|
|
||||||
csTools.WwwcRegionTool,
|
|
||||||
csTools.MagnifyTool,
|
|
||||||
csTools.StackScrollTool,
|
|
||||||
csTools.StackScrollMouseWheelTool,
|
|
||||||
csTools.OverlayTool,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function getTools() {
|
|
||||||
const tools = [];
|
|
||||||
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
|
|
||||||
tools.push(...toolsGroupedByType[toolsGroup])
|
|
||||||
);
|
|
||||||
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { toolsGroupedByType };
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import csTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
export default function _setActiveAndPassiveToolsForElement(element, tools) {
|
|
||||||
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
|
|
||||||
|
|
||||||
tools.forEach(tool => {
|
|
||||||
if (tool.prototype instanceof BaseAnnotationTool) {
|
|
||||||
// BaseAnnotationTool would likely come from csTools lib exports
|
|
||||||
const toolName = new tool().name;
|
|
||||||
csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 });
|
|
||||||
csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 });
|
|
||||||
csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 });
|
|
||||||
csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required
|
|
||||||
csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
|
||||||
csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {});
|
|
||||||
csTools.setToolEnabledForElement(element, 'Overlay', {});
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
|
|
||||||
|
|
||||||
export default function setCornerstoneMeasurementActive(measurement) {
|
|
||||||
const { id } = measurement;
|
|
||||||
|
|
||||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
|
||||||
|
|
||||||
Object.keys(toolState).forEach(imageId => {
|
|
||||||
const imageIdSpecificToolState = toolState[imageId];
|
|
||||||
|
|
||||||
Object.keys(imageIdSpecificToolState).forEach(toolType => {
|
|
||||||
const toolSpecificToolState = imageIdSpecificToolState[toolType];
|
|
||||||
|
|
||||||
const toolSpecificToolData = toolSpecificToolState.data;
|
|
||||||
|
|
||||||
if (toolSpecificToolData && toolSpecificToolData.length) {
|
|
||||||
toolSpecificToolData.forEach(data => {
|
|
||||||
data.active = data.id === id ? true : false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const enabledElements = cornerstoneTools.store.state.enabledElements;
|
|
||||||
|
|
||||||
enabledElements.forEach(element => {
|
|
||||||
try {
|
|
||||||
cornerstone.updateImage(element);
|
|
||||||
} catch (ex) {
|
|
||||||
// https://github.com/cornerstonejs/cornerstone/blob/master/src/updateImage.js#L16
|
|
||||||
// This fails if enabledElement.image is undefined and we have no layers
|
|
||||||
// Instead of throwing, it should _probably_ do nothing.
|
|
||||||
// We'll just swallow the exception
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,281 +1,204 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import {
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
getEnabledElement,
|
||||||
import OHIF from '@ohif/core';
|
StackViewport,
|
||||||
|
volumeLoader,
|
||||||
|
cache,
|
||||||
|
utilities as csUtils,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
import {
|
||||||
|
ToolGroupManager,
|
||||||
|
Enums,
|
||||||
|
segmentation,
|
||||||
|
utilities as csToolsUtils,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
|
|
||||||
//import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
|
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
||||||
import { getEnabledElement } from './state';
|
import callInputDialog from './utils/callInputDialog';
|
||||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||||
const scroll = cornerstoneTools.import('util/scroll');
|
|
||||||
|
|
||||||
const { studyMetadataManager } = OHIF.utils;
|
const commandsModule = ({ servicesManager }) => {
|
||||||
|
const {
|
||||||
|
ViewportGridService,
|
||||||
|
ToolGroupService,
|
||||||
|
CineService,
|
||||||
|
ToolBarService,
|
||||||
|
UIDialogService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
SegmentationService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
const imagePositionSynchronizer = new cornerstoneTools.Synchronizer(
|
function _getActiveViewportEnabledElement() {
|
||||||
cornerstone.EVENTS.NEW_IMAGE,
|
|
||||||
cornerstoneTools.stackImagePositionSynchronizer
|
|
||||||
);
|
|
||||||
|
|
||||||
const panZoomSynchronizer = new cornerstoneTools.Synchronizer(
|
|
||||||
cornerstone.EVENTS.IMAGE_RENDERED,
|
|
||||||
cornerstoneTools.panZoomSynchronizer
|
|
||||||
);
|
|
||||||
|
|
||||||
function onElementEnabledAddToSync(event) {
|
|
||||||
const { element } = event.detail;
|
|
||||||
|
|
||||||
imagePositionSynchronizer.add(element);
|
|
||||||
// panZoomSynchronizer.add(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onElementDisabledRemoveFromSync(event) {
|
|
||||||
const { element } = event.detail;
|
|
||||||
|
|
||||||
imagePositionSynchronizer.remove(element);
|
|
||||||
panZoomSynchronizer.remove(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
|
||||||
const { ViewportGridService } = servicesManager.services;
|
|
||||||
|
|
||||||
function _getActiveViewportsEnabledElement() {
|
|
||||||
const { activeViewportIndex } = ViewportGridService.getState();
|
const { activeViewportIndex } = ViewportGridService.getState();
|
||||||
const { element } = getEnabledElement(activeViewportIndex) || {};
|
const { element } = OHIFgetEnabledElement(activeViewportIndex) || {};
|
||||||
return element;
|
const enabledElement = getEnabledElement(element);
|
||||||
|
return enabledElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getToolGroup(toolGroupId) {
|
||||||
|
let toolGroupIdToUse = toolGroupId;
|
||||||
|
|
||||||
|
if (!toolGroupIdToUse) {
|
||||||
|
// Use the active viewport's tool group if no tool group id is provided
|
||||||
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { renderingEngineId, viewportId } = enabledElement;
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!toolGroup) {
|
||||||
|
console.warn(
|
||||||
|
'No tool group found for viewportId:',
|
||||||
|
viewportId,
|
||||||
|
'and renderingEngineId:',
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolGroupIdToUse = toolGroup.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolGroup = ToolGroupService.getToolGroup(toolGroupIdToUse);
|
||||||
|
return toolGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getCornerstoneLibraries: () => {
|
getActiveViewportEnabledElement: () => {
|
||||||
return { cornerstone, cornerstoneTools };
|
return _getActiveViewportEnabledElement();
|
||||||
},
|
},
|
||||||
rotateViewport: ({ rotation }) => {
|
setViewportActive: ({ viewportId }) => {
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
const viewportInfo = CornerstoneViewportService.getViewportInfo(
|
||||||
|
viewportId
|
||||||
if (enabledElement) {
|
);
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
if (!viewportInfo) {
|
||||||
viewport.rotation += rotation;
|
console.warn('No viewport found for viewportId:', viewportId);
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
return;
|
||||||
}
|
|
||||||
},
|
|
||||||
flipViewportHorizontal: () => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.hflip = !viewport.hflip;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
flipViewportVertical: () => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.vflip = !viewport.vflip;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scaleViewport: ({ direction }) => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
const step = direction * 0.15;
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
if (step) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.scale += step;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
} else {
|
|
||||||
cornerstone.fitToWindow(enabledElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetViewport: () => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
cornerstone.reset(enabledElement);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleSynchronizer: ({ toggledState }) => {
|
|
||||||
const synchronizers = [imagePositionSynchronizer];
|
|
||||||
// Set synchronizer state when the command is run.
|
|
||||||
synchronizers.forEach(s => {
|
|
||||||
s.enabled = toggledState;
|
|
||||||
});
|
|
||||||
|
|
||||||
const unsubscribe = () => {
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
|
||||||
onElementEnabledAddToSync
|
|
||||||
);
|
|
||||||
cornerstone.events.removeEventListener(
|
|
||||||
cornerstone.EVENTS.ELEMENT_DISABLED,
|
|
||||||
onElementDisabledRemoveFromSync
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const subscribe = () => {
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
|
||||||
onElementEnabledAddToSync
|
|
||||||
);
|
|
||||||
cornerstone.events.addEventListener(
|
|
||||||
cornerstone.EVENTS.ELEMENT_DISABLED,
|
|
||||||
onElementDisabledRemoveFromSync
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add event handlers so that if the layout is changed, new elements
|
|
||||||
// are automatically added to the synchronizer while it is enabled.
|
|
||||||
if (toggledState === true) {
|
|
||||||
subscribe();
|
|
||||||
} else {
|
|
||||||
// If the synchronizer is disabled, remove the event handlers
|
|
||||||
unsubscribe();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase existing state and then set up all currently existing elements
|
const viewportIndex = viewportInfo.getViewportIndex();
|
||||||
cornerstone.getEnabledElements().map(e => {
|
ViewportGridService.setActiveViewportIndex(viewportIndex);
|
||||||
synchronizers.forEach(s => {
|
},
|
||||||
s.remove(e.element);
|
arrowTextCallback: ({ callback, data }) => {
|
||||||
s.add(e.element);
|
callInputDialog(UIDialogService, data, callback);
|
||||||
|
},
|
||||||
|
toggleCine: () => {
|
||||||
|
const { viewports } = ViewportGridService.getState();
|
||||||
|
const { isCineEnabled } = CineService.getState();
|
||||||
|
CineService.setIsCineEnabled(!isCineEnabled);
|
||||||
|
ToolBarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
|
||||||
|
viewports.forEach((_, index) =>
|
||||||
|
CineService.setCine({ id: index, isPlaying: false })
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setWindowLevel({ window, level, toolGroupId }) {
|
||||||
|
// convert to numbers
|
||||||
|
const windowWidthNum = Number(window);
|
||||||
|
const windowCenterNum = Number(level);
|
||||||
|
|
||||||
|
const { viewportId } = _getActiveViewportEnabledElement();
|
||||||
|
const viewportToolGroupId = ToolGroupService.getToolGroupForViewport(
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get actor from the viewport
|
||||||
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
|
||||||
|
const lower = windowCenterNum - windowWidthNum / 2.0;
|
||||||
|
const upper = windowCenterNum + windowWidthNum / 2.0;
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
viewport.setProperties({
|
||||||
|
voiRange: {
|
||||||
|
upper,
|
||||||
|
lower,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
return unsubscribe;
|
|
||||||
},
|
|
||||||
invertViewport: ({ element }) => {
|
|
||||||
let enabledElement;
|
|
||||||
|
|
||||||
if (element === undefined) {
|
viewport.render();
|
||||||
enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
} else {
|
|
||||||
enabledElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabledElement) {
|
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
|
||||||
viewport.invert = !viewport.invert;
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancelMeasurement: () => {
|
toggleCrosshairs({ toolGroupId, toggledState }) {
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
const toolName = 'Crosshairs';
|
||||||
|
// If it is Enabled
|
||||||
|
if (toggledState) {
|
||||||
|
actions.setToolActive({ toolName, toolGroupId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toolGroup = _getToolGroup(toolGroupId);
|
||||||
|
|
||||||
if (enabledElement) {
|
if (!toolGroup) {
|
||||||
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
return;
|
||||||
'manipulatorState'
|
}
|
||||||
).setters.cancelActiveManipulatorsForElement;
|
|
||||||
|
|
||||||
cancelActiveManipulatorsForElement(enabledElement);
|
toolGroup.setToolDisabled(toolName);
|
||||||
|
|
||||||
cornerstone.updateImage(enabledElement);
|
// Get the primary toolId from the ToolBarService and set it to active
|
||||||
|
// Since it was set to passive if not already active
|
||||||
|
const primaryActiveTool = ToolBarService.state.primaryToolId;
|
||||||
|
if (
|
||||||
|
toolGroup?.toolOptions[primaryActiveTool]?.mode ===
|
||||||
|
Enums.ToolModes.Passive
|
||||||
|
) {
|
||||||
|
toolGroup.setToolActive(primaryActiveTool, {
|
||||||
|
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||||
// better mouseButtonMask sets.
|
const toolGroup = _getToolGroup(toolGroupId);
|
||||||
setToolActive: ({ toolName }) => {
|
|
||||||
if (!toolName) {
|
if (!toolGroup) {
|
||||||
console.warn('No toolname provided to setToolActive command');
|
console.warn('No tool group found for toolGroupId:', toolGroupId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
// Todo: we need to check if the viewports of the toolGroup is actually
|
||||||
|
// parts of the ViewportGrid's viewports, if not we return
|
||||||
|
|
||||||
// Find total number of tool indexes
|
const { viewports } = ViewportGridService.getState() || {
|
||||||
const { viewports } = ViewportGridService.getState() || { viewports: [] };
|
viewports: [],
|
||||||
for (let i = 0; i < viewports.length; i++) {
|
};
|
||||||
const viewport = viewports[i];
|
|
||||||
const hasDisplaySet = viewport.displaySetInstanceUID !== undefined;
|
|
||||||
|
|
||||||
if (!hasDisplaySet) {
|
// iterate over all viewports and set the tool active for the
|
||||||
|
// viewports that belong to the toolGroup
|
||||||
|
for (let index = 0; index < viewports.length; index++) {
|
||||||
|
const ohifEnabledElement = OHIFgetEnabledElement(index);
|
||||||
|
|
||||||
|
if (!ohifEnabledElement) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportInfo = getEnabledElement(i);
|
const viewport = getEnabledElement(ohifEnabledElement.element);
|
||||||
if (!viewportInfo) continue;
|
|
||||||
const hasCornerstoneContext =
|
|
||||||
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
|
|
||||||
|
|
||||||
if (hasCornerstoneContext) {
|
if (!viewport) {
|
||||||
cornerstoneTools.setToolActiveForElement(
|
continue;
|
||||||
viewportInfo.element,
|
|
||||||
toolName,
|
|
||||||
{ mouseButtonMask: 1 }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
commandsManager.runCommand(
|
|
||||||
'setToolActive',
|
|
||||||
{
|
|
||||||
element: viewportInfo.element,
|
|
||||||
toolName,
|
|
||||||
},
|
|
||||||
viewportInfo.context
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
clearAnnotations: () => {
|
|
||||||
const element = _getActiveViewportsEnabledElement();
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { enabledElement } = cornerstone.getEnabledElement(element) || {};
|
// Find the current active tool and set it to be passive
|
||||||
if (!enabledElement || !enabledElement.image) {
|
const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
if (activeTool) {
|
||||||
toolState,
|
toolGroup.setToolPassive(activeTool);
|
||||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
}
|
||||||
if (
|
|
||||||
!toolState ||
|
|
||||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
// Set the new toolName to be active
|
||||||
|
toolGroup.setToolActive(toolName, {
|
||||||
const measurementsToRemove = [];
|
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||||
|
|
||||||
Object.keys(imageIdToolState).forEach(toolType => {
|
|
||||||
const { data } = imageIdToolState[toolType];
|
|
||||||
|
|
||||||
data.forEach(measurementData => {
|
|
||||||
const {
|
|
||||||
_id,
|
|
||||||
lesionNamingNumber,
|
|
||||||
measurementNumber,
|
|
||||||
} = measurementData;
|
|
||||||
|
|
||||||
if (!_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
measurementsToRemove.push({
|
|
||||||
toolType,
|
|
||||||
_id,
|
|
||||||
lesionNamingNumber,
|
|
||||||
measurementNumber,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
measurementsToRemove.forEach(measurementData => {
|
return;
|
||||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
}
|
||||||
detail: {
|
|
||||||
toolType: measurementData.toolType,
|
|
||||||
measurementData,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
nextImage: () => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
scroll(enabledElement, 1);
|
|
||||||
},
|
|
||||||
previousImage: () => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
scroll(enabledElement, -1);
|
|
||||||
},
|
|
||||||
getActiveViewportEnabledElement: () => {
|
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
|
||||||
return enabledElement;
|
|
||||||
},
|
},
|
||||||
showDownloadViewportModal: () => {
|
showDownloadViewportModal: () => {
|
||||||
const { activeViewportIndex } = ViewportGridService.getState();
|
const { activeViewportIndex } = ViewportGridService.getState();
|
||||||
@ -288,119 +211,215 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
contentProps: {
|
contentProps: {
|
||||||
activeViewportIndex,
|
activeViewportIndex,
|
||||||
onClose: UIModalService.hide,
|
onClose: UIModalService.hide,
|
||||||
|
CornerstoneViewportService,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
|
rotateViewport: ({ rotation }) => {
|
||||||
const nearbyTool = {};
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
let pointNearTool = false;
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
availableToolTypes.forEach(toolType => {
|
const { viewport } = enabledElement;
|
||||||
const elementToolData = cornerstoneTools.getToolState(
|
|
||||||
element,
|
|
||||||
toolType
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!elementToolData) {
|
if (viewport instanceof StackViewport) {
|
||||||
return;
|
const { rotation: currentRotation } = viewport.getProperties();
|
||||||
|
const newRotation = (currentRotation + rotation) % 360;
|
||||||
|
viewport.setProperties({ rotation: newRotation });
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flipViewportHorizontal: () => {
|
||||||
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
const { flipHorizontal } = viewport.getCamera();
|
||||||
|
viewport.setCamera({ flipHorizontal: !flipHorizontal });
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flipViewportVertical: () => {
|
||||||
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
const { flipVertical } = viewport.getCamera();
|
||||||
|
viewport.setCamera({ flipVertical: !flipVertical });
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
invertViewport: ({ element }) => {
|
||||||
|
let enabledElement;
|
||||||
|
|
||||||
|
if (element === undefined) {
|
||||||
|
enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
} else {
|
||||||
|
enabledElement = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
const { invert } = viewport.getProperties();
|
||||||
|
viewport.setProperties({ invert: !invert });
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetViewport: () => {
|
||||||
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
viewport.resetProperties();
|
||||||
|
viewport.resetCamera();
|
||||||
|
viewport.render();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scaleViewport: ({ direction }) => {
|
||||||
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
const scaleFactor = direction > 0 ? 0.9 : 1.1;
|
||||||
|
|
||||||
|
if (!enabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
if (direction) {
|
||||||
|
const { parallelScale } = viewport.getCamera();
|
||||||
|
viewport.setCamera({ parallelScale: parallelScale * scaleFactor });
|
||||||
|
viewport.render();
|
||||||
|
} else {
|
||||||
|
viewport.resetCamera();
|
||||||
|
viewport.render();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scroll: ({ direction }) => {
|
||||||
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
|
|
||||||
elementToolData.data.forEach((toolData, index) => {
|
if (!enabledElement) {
|
||||||
let elementToolInstance = cornerstoneTools.getToolForElement(
|
return;
|
||||||
element,
|
}
|
||||||
toolType
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!elementToolInstance) {
|
const { viewport } = enabledElement;
|
||||||
elementToolInstance = cornerstoneTools.getToolForElement(
|
const options = { delta: direction };
|
||||||
element,
|
|
||||||
`${toolType}Tool`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!elementToolInstance) {
|
csToolsUtils.scroll(viewport, options);
|
||||||
console.warn('Tool not found.');
|
},
|
||||||
return undefined;
|
async createSegmentationForDisplaySet({ displaySetInstanceUID }) {
|
||||||
}
|
const volumeId = displaySetInstanceUID;
|
||||||
|
|
||||||
if (
|
const segmentationUID = csUtils.uuidv4();
|
||||||
elementToolInstance.pointNearTool(
|
const segmentationId = `${volumeId}::${segmentationUID}`;
|
||||||
element,
|
|
||||||
toolData,
|
|
||||||
canvasCoordinates
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
pointNearTool = true;
|
|
||||||
nearbyTool.tool = toolData;
|
|
||||||
nearbyTool.index = index;
|
|
||||||
nearbyTool.toolType = toolType;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pointNearTool) {
|
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
|
||||||
return false;
|
volumeId: segmentationId,
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return pointNearTool ? nearbyTool : undefined;
|
// Add the segmentations to state
|
||||||
},
|
segmentation.addSegmentations([
|
||||||
removeToolState: ({ element, toolType, tool }) => {
|
{
|
||||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
segmentationId,
|
||||||
cornerstone.updateImage(element);
|
representation: {
|
||||||
},
|
// The type of segmentation
|
||||||
// setCornerstoneLayout: () => {
|
type: Enums.SegmentationRepresentations.Labelmap,
|
||||||
// setCornerstoneLayout();
|
// The actual segmentation data, in the case of labelmap this is a
|
||||||
// },
|
// reference to the source volume of the segmentation.
|
||||||
setWindowLevel: ({ window, level }) => {
|
data: {
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
volumeId: segmentationId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (enabledElement) {
|
return segmentationId;
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
},
|
||||||
|
async addSegmentationRepresentationToToolGroup({
|
||||||
|
segmentationId,
|
||||||
|
toolGroupId,
|
||||||
|
representationType,
|
||||||
|
}) {
|
||||||
|
// // Add the segmentation representation to the toolgroup
|
||||||
|
await segmentation.addSegmentationRepresentations(toolGroupId, [
|
||||||
|
{
|
||||||
|
segmentationId,
|
||||||
|
type: representationType,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
getLabelmapVolumes: ({ segmentations }) => {
|
||||||
|
if (!segmentations || !segmentations.length) {
|
||||||
|
segmentations = SegmentationService.getSegmentations();
|
||||||
|
}
|
||||||
|
|
||||||
viewport.voi = {
|
const labelmapVolumes = segmentations.map(segmentation => {
|
||||||
windowWidth: Number(window),
|
return cache.getVolume(segmentation.id);
|
||||||
windowCenter: Number(level),
|
});
|
||||||
};
|
|
||||||
cornerstone.setViewport(enabledElement, viewport);
|
return labelmapVolumes;
|
||||||
|
},
|
||||||
|
setViewportColormap: ({
|
||||||
|
viewportIndex,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
colormap,
|
||||||
|
immediate = false,
|
||||||
|
}) => {
|
||||||
|
const viewport = CornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const actorEntries = viewport.getActors();
|
||||||
|
|
||||||
|
const actorEntry = actorEntries.find(actorEntry => {
|
||||||
|
return actorEntry.uid === displaySetInstanceUID;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { actor: volumeActor } = actorEntry;
|
||||||
|
|
||||||
|
setColormap(volumeActor, colormap);
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
viewport.render();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
jumpToImage: {
|
setWindowLevel: {
|
||||||
commandFn: actions.jumpToImage,
|
commandFn: actions.setWindowLevel,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
getCornerstoneLibraries: {
|
setToolActive: {
|
||||||
commandFn: actions.getCornerstoneLibraries,
|
commandFn: actions.setToolActive,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
context: 'VIEWER',
|
|
||||||
},
|
|
||||||
getNearbyToolData: {
|
|
||||||
commandFn: actions.getNearbyToolData,
|
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
toggleSynchronizer: {
|
toggleCrosshairs: {
|
||||||
commandFn: actions.toggleSynchronizer,
|
commandFn: actions.toggleCrosshairs,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
removeToolState: {
|
|
||||||
commandFn: actions.removeToolState,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
showDownloadViewportModal: {
|
|
||||||
commandFn: actions.showDownloadViewportModal,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
getActiveViewportEnabledElement: {
|
|
||||||
commandFn: actions.getActiveViewportEnabledElement,
|
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
@ -414,13 +433,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { rotation: -90 },
|
options: { rotation: -90 },
|
||||||
},
|
},
|
||||||
invertViewport: {
|
flipViewportHorizontal: {
|
||||||
commandFn: actions.invertViewport,
|
commandFn: actions.flipViewportHorizontal,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
cancelMeasurement: {
|
|
||||||
commandFn: actions.cancelMeasurement,
|
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
@ -429,8 +443,13 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
flipViewportHorizontal: {
|
invertViewport: {
|
||||||
commandFn: actions.flipViewportHorizontal,
|
commandFn: actions.invertViewport,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
resetViewport: {
|
||||||
|
commandFn: actions.resetViewport,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
@ -449,45 +468,54 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { direction: 0 },
|
options: { direction: 0 },
|
||||||
},
|
},
|
||||||
resetViewport: {
|
|
||||||
commandFn: actions.resetViewport,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
clearAnnotations: {
|
|
||||||
commandFn: actions.clearAnnotations,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
nextImage: {
|
nextImage: {
|
||||||
commandFn: actions.nextImage,
|
commandFn: actions.scroll,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: { direction: 1 },
|
||||||
},
|
},
|
||||||
previousImage: {
|
previousImage: {
|
||||||
commandFn: actions.previousImage,
|
commandFn: actions.scroll,
|
||||||
|
storeContexts: [],
|
||||||
|
options: { direction: -1 },
|
||||||
|
},
|
||||||
|
showDownloadViewportModal: {
|
||||||
|
commandFn: actions.showDownloadViewportModal,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
// TOOLS
|
toggleCine: {
|
||||||
setToolActive: {
|
commandFn: actions.toggleCine,
|
||||||
commandFn: actions.setToolActive,
|
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
setZoomTool: {
|
arrowTextCallback: {
|
||||||
commandFn: actions.setToolActive,
|
commandFn: actions.arrowTextCallback,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { toolName: 'Zoom' },
|
options: {},
|
||||||
},
|
},
|
||||||
// setCornerstoneLayout: {
|
setViewportActive: {
|
||||||
// commandFn: actions.setCornerstoneLayout,
|
commandFn: actions.setViewportActive,
|
||||||
// storeContexts: [],
|
storeContexts: [],
|
||||||
// options: {},
|
options: {},
|
||||||
// context: 'VIEWER',
|
},
|
||||||
// },
|
createSegmentationForDisplaySet: {
|
||||||
setWindowLevel: {
|
commandFn: actions.createSegmentationForDisplaySet,
|
||||||
commandFn: actions.setWindowLevel,
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
addSegmentationRepresentationToToolGroup: {
|
||||||
|
commandFn: actions.addSegmentationRepresentationToToolGroup,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
|
||||||
|
getLabelmapVolumes: {
|
||||||
|
commandFn: actions.getLabelmapVolumes,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
setViewportColormap: {
|
||||||
|
commandFn: actions.setViewportColormap,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
@ -496,7 +524,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
return {
|
return {
|
||||||
actions,
|
actions,
|
||||||
definitions,
|
definitions,
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
defaultContext: 'CORNERSTONE',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,61 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import init from './init.js';
|
|
||||||
import commandsModule from './commandsModule.js';
|
|
||||||
import { id } from './id.js';
|
|
||||||
// import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
|
||||||
return import(/* webpackPrefetch: true */ './OHIFCornerstoneViewport');
|
|
||||||
});
|
|
||||||
|
|
||||||
const OHIFCornerstoneViewport = props => {
|
|
||||||
return (
|
|
||||||
<React.Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<Component {...props} />
|
|
||||||
</React.Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
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, commandsManager, configuration = {} }) {
|
|
||||||
init({ servicesManager, commandsManager, configuration });
|
|
||||||
},
|
|
||||||
getViewportModule({ servicesManager, commandsManager }) {
|
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
|
||||||
const onNewImageHandler = jumpData => {
|
|
||||||
commandsManager.runCommand('jumpToImage', jumpData);
|
|
||||||
};
|
|
||||||
const { ToolBarService } = servicesManager.services;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OHIFCornerstoneViewport
|
|
||||||
{...props}
|
|
||||||
ToolBarService={ToolBarService}
|
|
||||||
servicesManager={servicesManager}
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
onNewImage={onNewImageHandler}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ name: 'cornerstone', component: ExtendedOHIFCornerstoneViewport },
|
|
||||||
];
|
|
||||||
},
|
|
||||||
getCommandsModule({ servicesManager, commandsManager }) {
|
|
||||||
return commandsModule({ servicesManager, commandsManager });
|
|
||||||
},
|
|
||||||
};
|
|
||||||
136
extensions/cornerstone/src/index.tsx
Normal file
136
extensions/cornerstone/src/index.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
|
import {
|
||||||
|
Enums as cs3DEnums,
|
||||||
|
CONSTANTS,
|
||||||
|
imageLoadPoolManager,
|
||||||
|
imageRetrievalPoolManager,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
|
import init from './init.js';
|
||||||
|
import commandsModule from './commandsModule';
|
||||||
|
import ToolGroupService from './services/ToolGroupService';
|
||||||
|
import SyncGroupService from './services/SyncGroupService';
|
||||||
|
import { toolNames } from './initCornerstoneTools';
|
||||||
|
import { getEnabledElement } from './state';
|
||||||
|
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||||
|
import dicomLoaderService from './utils/dicomLoaderService';
|
||||||
|
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||||
|
|
||||||
|
import { id } from './id';
|
||||||
|
|
||||||
|
const Component = React.lazy(() => {
|
||||||
|
return import(
|
||||||
|
/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const OHIFCornerstoneViewport = props => {
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<Component {...props} />
|
||||||
|
</React.Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const cornerstoneExtension = {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
|
||||||
|
onModeExit: () => {
|
||||||
|
// Empty out the image load and retrieval pools to prevent memory leaks
|
||||||
|
// on the mode exits
|
||||||
|
Object.values(cs3DEnums.RequestType).forEach(type => {
|
||||||
|
imageLoadPoolManager.clearRequestStack(type);
|
||||||
|
imageRetrievalPoolManager.clearRequestStack(type);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
async preRegistration({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
configuration = {},
|
||||||
|
appConfig,
|
||||||
|
}) {
|
||||||
|
servicesManager.registerService(
|
||||||
|
CornerstoneViewportService(servicesManager)
|
||||||
|
);
|
||||||
|
servicesManager.registerService(ToolGroupService(servicesManager));
|
||||||
|
servicesManager.registerService(SyncGroupService(servicesManager));
|
||||||
|
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||||
|
},
|
||||||
|
getViewportModule({ servicesManager, commandsManager }) {
|
||||||
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
|
// const onNewImageHandler = jumpData => {
|
||||||
|
// commandsManager.runCommand('jumpToImage', jumpData);
|
||||||
|
// };
|
||||||
|
const { ToolBarService } = servicesManager.services;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OHIFCornerstoneViewport
|
||||||
|
{...props}
|
||||||
|
ToolBarService={ToolBarService}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'cornerstone',
|
||||||
|
component: ExtendedOHIFCornerstoneViewport,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||||
|
return commandsModule({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getUtilityModule({ servicesManager }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'common',
|
||||||
|
exports: {
|
||||||
|
getCornerstoneLibraries: () => {
|
||||||
|
return { cornerstone, cornerstoneTools };
|
||||||
|
},
|
||||||
|
getEnabledElement,
|
||||||
|
dicomLoaderService,
|
||||||
|
registerColormap,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'core',
|
||||||
|
exports: {
|
||||||
|
Enums: cs3DEnums,
|
||||||
|
CONSTANTS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tools',
|
||||||
|
exports: {
|
||||||
|
toolNames,
|
||||||
|
Enums: cs3DToolsEnums,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cornerstoneExtension;
|
||||||
@ -1,86 +1,126 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import { ContextMenuMeasurements } from '@ohif/ui';
|
import { ContextMenuMeasurements } from '@ohif/ui';
|
||||||
import cs from 'cornerstone-core';
|
|
||||||
import csTools from 'cornerstone-tools';
|
|
||||||
import merge from 'lodash.merge';
|
|
||||||
import getTools, { toolsGroupedByType } from './utils/getTools.js';
|
|
||||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
|
||||||
import initWADOImageLoader from './initWADOImageLoader.js';
|
|
||||||
import getCornerstoneMeasurementById from './utils/getCornerstoneMeasurementById';
|
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
|
||||||
import { setEnabledElement } from './state';
|
|
||||||
import callInputDialog from './callInputDialog.js';
|
|
||||||
|
|
||||||
// TODO -> Global "context menu open state", or lots of expensive searches on drag?
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
|
import {
|
||||||
|
init as cs3DInit,
|
||||||
|
eventTarget,
|
||||||
|
EVENTS,
|
||||||
|
metaData,
|
||||||
|
volumeLoader,
|
||||||
|
imageLoader,
|
||||||
|
imageLoadPoolManager,
|
||||||
|
Settings,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
import { Enums, utilities } from '@cornerstonejs/tools';
|
||||||
|
import {
|
||||||
|
cornerstoneStreamingImageVolumeLoader,
|
||||||
|
sharedArrayBufferImageLoader,
|
||||||
|
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
|
|
||||||
|
import initWADOImageLoader from './initWADOImageLoader';
|
||||||
|
import initCornerstoneTools from './initCornerstoneTools';
|
||||||
|
|
||||||
|
import { connectToolsToMeasurementService } from './initMeasurementService';
|
||||||
|
import callInputDialog from './utils/callInputDialog';
|
||||||
|
import initCineService from './initCineService';
|
||||||
|
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
||||||
|
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||||
|
import initSegmentationService from './initSegmentationService';
|
||||||
|
|
||||||
|
const cs3DToolsEvents = Enums.Events;
|
||||||
|
|
||||||
let CONTEXT_MENU_OPEN = false;
|
let CONTEXT_MENU_OPEN = false;
|
||||||
const { globalImageIdSpecificToolStateManager } = csTools;
|
|
||||||
|
|
||||||
const TOOL_TYPES_WITH_CONTEXT_MENU = [
|
|
||||||
'Angle',
|
|
||||||
'ArrowAnnotate',
|
|
||||||
'Bidirectional',
|
|
||||||
'Length',
|
|
||||||
'FreehandMouse',
|
|
||||||
'EllipticalRoi',
|
|
||||||
'CircleRoi',
|
|
||||||
'RectangleRoi',
|
|
||||||
// SR Viewport...
|
|
||||||
'SRLength',
|
|
||||||
'SRBidirectional',
|
|
||||||
'SRArrowAnnotate',
|
|
||||||
'SREllipticalRoi',
|
|
||||||
];
|
|
||||||
|
|
||||||
const _refreshViewports = () =>
|
|
||||||
cs.getEnabledElements().forEach(({ element }) => cs.updateImage(element));
|
|
||||||
|
|
||||||
/* Add extension tools configuration here. */
|
|
||||||
const _createInternalToolsConfig = UIDialogService => {
|
|
||||||
return {
|
|
||||||
ArrowAnnotate: {
|
|
||||||
configuration: {
|
|
||||||
getTextCallback: (callback, eventDetails) =>
|
|
||||||
callInputDialog(UIDialogService, null, callback),
|
|
||||||
changeTextCallback: (data, eventDetails, callback) =>
|
|
||||||
callInputDialog(UIDialogService, data, callback),
|
|
||||||
allowEmptyLabel: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DragProbe: {
|
|
||||||
defaultStrategy: 'minimal',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// TODO: Cypress tests are currently grabbing this from the window?
|
||||||
|
window.cornerstone = cornerstone;
|
||||||
|
window.cornerstoneTools = cornerstoneTools;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Object} servicesManager
|
|
||||||
* @param {Object} configuration
|
|
||||||
* @param {Object|Array} configuration.csToolsConfig
|
|
||||||
*/
|
*/
|
||||||
export default function init({
|
export default async function init({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
configuration,
|
configuration,
|
||||||
|
appConfig,
|
||||||
}) {
|
}) {
|
||||||
|
await cs3DInit();
|
||||||
|
|
||||||
|
// For debugging e2e tests that are failing on CI
|
||||||
|
//cornerstone.setUseCPURendering(true);
|
||||||
|
|
||||||
|
// For debugging large datasets
|
||||||
|
//cornerstone.cache.setMaxCacheSize(3000000000);
|
||||||
|
|
||||||
|
initCornerstoneTools();
|
||||||
|
|
||||||
|
// Don't use cursors in viewports
|
||||||
|
// Todo: this should come from extension/app configuration
|
||||||
|
Settings.getRuntimeSettings().set('useCursors', false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
UIDialogService,
|
UserAuthenticationService,
|
||||||
|
ToolGroupService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
ToolBarService,
|
UIDialogService,
|
||||||
UserAuthenticationService,
|
CineService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
HangingProtocolService,
|
||||||
|
SegmentationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
const tools = getTools();
|
|
||||||
|
|
||||||
console.log(servicesManager.services);
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||||
|
|
||||||
|
volumeLoader.registerUnknownVolumeLoader(
|
||||||
|
cornerstoneStreamingImageVolumeLoader
|
||||||
|
);
|
||||||
|
volumeLoader.registerVolumeLoader(
|
||||||
|
'cornerstoneStreamingImageVolume',
|
||||||
|
cornerstoneStreamingImageVolumeLoader
|
||||||
|
);
|
||||||
|
|
||||||
|
HangingProtocolService.registerImageLoadStrategy(
|
||||||
|
'interleaveCenter',
|
||||||
|
interleaveCenterLoader
|
||||||
|
);
|
||||||
|
HangingProtocolService.registerImageLoadStrategy(
|
||||||
|
'interleaveTopToBottom',
|
||||||
|
interleaveTopToBottom
|
||||||
|
);
|
||||||
|
|
||||||
|
imageLoader.registerImageLoader(
|
||||||
|
'streaming-wadors',
|
||||||
|
sharedArrayBufferImageLoader
|
||||||
|
);
|
||||||
|
|
||||||
|
metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
||||||
|
|
||||||
|
imageLoadPoolManager.maxNumRequests = {
|
||||||
|
interaction: appConfig?.maxNumRequests?.interaction || 100,
|
||||||
|
thumbnail: appConfig?.maxNumRequests?.thumbnail || 75,
|
||||||
|
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
initWADOImageLoader(UserAuthenticationService, appConfig);
|
||||||
|
|
||||||
/* Measurement Service */
|
/* Measurement Service */
|
||||||
const measurementServiceSource = _connectToolsToMeasurementService(
|
const measurementServiceSource = connectToolsToMeasurementService(
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
initSegmentationService(SegmentationService, CornerstoneViewportService);
|
||||||
|
|
||||||
|
initCineService(CineService);
|
||||||
|
|
||||||
|
const _getDefaultPosition = event => ({
|
||||||
|
x: (event && event.currentPoints.client[0]) || 0,
|
||||||
|
y: (event && event.currentPoints.client[1]) || 0,
|
||||||
|
});
|
||||||
|
|
||||||
const onRightClick = event => {
|
const onRightClick = event => {
|
||||||
if (!UIDialogService) {
|
if (!UIDialogService) {
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||||
@ -89,16 +129,17 @@ export default function init({
|
|||||||
|
|
||||||
const onGetMenuItems = defaultMenuItems => {
|
const onGetMenuItems = defaultMenuItems => {
|
||||||
const { element, currentPoints } = event.detail;
|
const { element, currentPoints } = event.detail;
|
||||||
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
|
|
||||||
|
const nearbyToolData = utilities.getAnnotationNearPoint(
|
||||||
element,
|
element,
|
||||||
canvasCoordinates: currentPoints.canvas,
|
currentPoints.canvas
|
||||||
availableToolTypes: TOOL_TYPES_WITH_CONTEXT_MENU,
|
);
|
||||||
});
|
|
||||||
|
|
||||||
let menuItems = [];
|
let menuItems = [];
|
||||||
if (nearbyToolData) {
|
if (nearbyToolData) {
|
||||||
defaultMenuItems.forEach(item => {
|
defaultMenuItems.forEach(item => {
|
||||||
item.value = nearbyToolData;
|
item.value = nearbyToolData;
|
||||||
|
item.element = element;
|
||||||
menuItems.push(item);
|
menuItems.push(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -123,41 +164,15 @@ export default function init({
|
|||||||
onGetMenuItems,
|
onGetMenuItems,
|
||||||
eventData: event.detail,
|
eventData: event.detail,
|
||||||
onDelete: item => {
|
onDelete: item => {
|
||||||
const { tool: measurementData, toolType } = item.value;
|
const { annotationUID } = item.value;
|
||||||
|
|
||||||
|
const uid = annotationUID;
|
||||||
// Sync'd w/ Measurement Service
|
// Sync'd w/ Measurement Service
|
||||||
if (measurementData.id) {
|
if (uid) {
|
||||||
measurementServiceSource.remove(measurementData.id);
|
measurementServiceSource.remove(uid, {
|
||||||
}
|
element: item.element,
|
||||||
// Only in cstools
|
|
||||||
else {
|
|
||||||
const toolState =
|
|
||||||
csTools.globalImageIdSpecificToolStateManager.toolState;
|
|
||||||
|
|
||||||
Object.keys(toolState).forEach(imageId => {
|
|
||||||
Object.keys(toolState[imageId]).forEach(imageToolType => {
|
|
||||||
const numMeasurements =
|
|
||||||
toolState[imageId][imageToolType].data.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < numMeasurements; i++) {
|
|
||||||
const toolData = toolState[imageId][imageToolType].data[i];
|
|
||||||
if (measurementData.uuid === toolData.uuid) {
|
|
||||||
// Clear and nuke matching measurement
|
|
||||||
toolState[imageId][imageToolType].data[i] = null;
|
|
||||||
delete toolState[imageId][imageToolType].data[i];
|
|
||||||
toolState[imageId][imageToolType].data.splice(i, 1);
|
|
||||||
|
|
||||||
// Delete "data" if we deleted last measurement in array
|
|
||||||
// if (toolState[imageId][imageToolType].data.length === 0) {
|
|
||||||
// delete toolState[imageId][imageToolType].data;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
console.log(csTools.store.state);
|
|
||||||
}
|
}
|
||||||
_refreshViewports();
|
|
||||||
CONTEXT_MENU_OPEN = false;
|
CONTEXT_MENU_OPEN = false;
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
@ -165,11 +180,9 @@ export default function init({
|
|||||||
UIDialogService.dismiss({ id: 'context-menu' });
|
UIDialogService.dismiss({ id: 'context-menu' });
|
||||||
},
|
},
|
||||||
onSetLabel: item => {
|
onSetLabel: item => {
|
||||||
const { tool: measurementData } = item.value;
|
const { annotationUID } = item.value;
|
||||||
|
|
||||||
const measurement = MeasurementService.getMeasurement(
|
const measurement = MeasurementService.getMeasurement(annotationUID);
|
||||||
measurementData.id
|
|
||||||
);
|
|
||||||
|
|
||||||
callInputDialog(
|
callInputDialog(
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
@ -184,7 +197,7 @@ export default function init({
|
|||||||
});
|
});
|
||||||
|
|
||||||
MeasurementService.update(
|
MeasurementService.update(
|
||||||
updatedMeasurement.id,
|
updatedMeasurement.uid,
|
||||||
updatedMeasurement,
|
updatedMeasurement,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@ -198,19 +211,6 @@ export default function init({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTouchPress = event => {
|
|
||||||
if (!UIDialogService) {
|
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIDialogService.create({
|
|
||||||
eventData: event.detail,
|
|
||||||
content: ContextMenuMeasurements,
|
|
||||||
contentProps: { isTouchEvent: true },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetContextMenu = () => {
|
const resetContextMenu = () => {
|
||||||
if (!UIDialogService) {
|
if (!UIDialogService) {
|
||||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||||
@ -222,6 +222,24 @@ export default function init({
|
|||||||
UIDialogService.dismiss({ id: 'context-menu' });
|
UIDialogService.dismiss({ id: 'context-menu' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When a custom image load is performed, update the relevant viewports
|
||||||
|
HangingProtocolService.subscribe(
|
||||||
|
HangingProtocolService.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED,
|
||||||
|
volumeInputArrayMap => {
|
||||||
|
for (const entry of volumeInputArrayMap.entries()) {
|
||||||
|
const [viewportId, volumeInputArray] = entry;
|
||||||
|
const viewport = CornerstoneViewportService.getCornerstoneViewport(
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
CornerstoneViewportService.setVolumesForViewport(
|
||||||
|
viewport,
|
||||||
|
volumeInputArray
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Because click gives us the native "mouse up", buttons will always be `0`
|
* Because click gives us the native "mouse up", buttons will always be `0`
|
||||||
* Need to fallback to event.which;
|
* Need to fallback to event.which;
|
||||||
@ -235,445 +253,50 @@ export default function init({
|
|||||||
clickMethodHandler(evt);
|
clickMethodHandler(evt);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelContextMenuIfOpen = evt => {
|
// const cancelContextMenuIfOpen = evt => {
|
||||||
if (CONTEXT_MENU_OPEN) {
|
// if (CONTEXT_MENU_OPEN) {
|
||||||
resetContextMenu();
|
// resetContextMenu();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
// TODO: This is the handler for ALL ENABLED ELEMENT EVENTS
|
function elementEnabledHandler(evt) {
|
||||||
// ... Activation logic should take place per element, not for all (diff behavior per ext)
|
const { element } = evt.detail;
|
||||||
function elementEnabledHandler(tools, evt) {
|
|
||||||
const element = evt.detail.element;
|
|
||||||
|
|
||||||
_addConfiguredToolsForElement(
|
|
||||||
UIDialogService,
|
|
||||||
element,
|
|
||||||
tools,
|
|
||||||
configuration
|
|
||||||
);
|
|
||||||
|
|
||||||
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
csTools.EVENTS.MOUSE_CLICK,
|
cs3DToolsEvents.MOUSE_CLICK,
|
||||||
contextMenuHandleClick
|
contextMenuHandleClick
|
||||||
);
|
);
|
||||||
element.addEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
|
|
||||||
|
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
||||||
|
const { element } = evt.detail;
|
||||||
|
utilities.stackPrefetch.enable(element);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementDisabledHandler(evt) {
|
function elementDisabledHandler(evt) {
|
||||||
const element = evt.detail.element;
|
const { viewportId, element } = evt.detail;
|
||||||
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
ToolGroupService.disable(viewportInfo);
|
||||||
|
|
||||||
element.removeEventListener(
|
element.removeEventListener(
|
||||||
csTools.EVENTS.MOUSE_CLICK,
|
cs3DToolsEvents.MOUSE_CLICK,
|
||||||
contextMenuHandleClick
|
contextMenuHandleClick
|
||||||
);
|
);
|
||||||
element.removeEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { csToolsConfig } = configuration;
|
eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
||||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
const { element } = evt.detail;
|
||||||
|
utilities.stackPrefetch.disable(element);
|
||||||
cs.metaData.addProvider(metadataProvider.get.bind(metadataProvider), 9999);
|
|
||||||
|
|
||||||
initWADOImageLoader(UserAuthenticationService);
|
|
||||||
|
|
||||||
// ~~
|
|
||||||
const defaultCsToolsConfig = csToolsConfig || {
|
|
||||||
globalToolSyncEnabled: false, // hold on to your pants!
|
|
||||||
showSVGCursors: false,
|
|
||||||
autoResizeViewports: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
initCornerstoneTools(defaultCsToolsConfig);
|
|
||||||
// TODO: Extensions are still registered at time of registration globally
|
|
||||||
// These should be registered as a part of mode route spin up,
|
|
||||||
// and they need to self-clean on mode route destroy
|
|
||||||
// THIS
|
|
||||||
// is a way for extensions that "depend" on this extension to notify it of
|
|
||||||
// new cornerstone enabled elements so it's commands continue to work.
|
|
||||||
const handleOhifCornerstoneEnabledElementEvent = function(evt) {
|
|
||||||
const { context, viewportIndex, enabledElement } = evt.detail;
|
|
||||||
|
|
||||||
setEnabledElement(viewportIndex, enabledElement, context);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener(
|
|
||||||
'ohif-cornerstone-enabled-element-event',
|
|
||||||
handleOhifCornerstoneEnabledElementEvent
|
|
||||||
);
|
|
||||||
|
|
||||||
cs.events.addEventListener(
|
|
||||||
cs.EVENTS.ELEMENT_ENABLED,
|
|
||||||
elementEnabledHandler.bind(null, tools)
|
|
||||||
);
|
|
||||||
cs.events.addEventListener(
|
|
||||||
cs.EVENTS.ELEMENT_DISABLED,
|
|
||||||
elementDisabledHandler
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _initMeasurementService = (MeasurementService, DisplaySetService) => {
|
|
||||||
/* Initialization */
|
|
||||||
const {
|
|
||||||
Length,
|
|
||||||
Bidirectional,
|
|
||||||
EllipticalRoi,
|
|
||||||
ArrowAnnotate,
|
|
||||||
} = measurementServiceMappingsFactory(MeasurementService, DisplaySetService);
|
|
||||||
const csToolsVer4MeasurementSource = MeasurementService.createSource(
|
|
||||||
'CornerstoneTools',
|
|
||||||
'4'
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Mappings */
|
|
||||||
MeasurementService.addMapping(
|
|
||||||
csToolsVer4MeasurementSource,
|
|
||||||
'Length',
|
|
||||||
Length.matchingCriteria,
|
|
||||||
Length.toAnnotation,
|
|
||||||
Length.toMeasurement
|
|
||||||
);
|
|
||||||
|
|
||||||
MeasurementService.addMapping(
|
|
||||||
csToolsVer4MeasurementSource,
|
|
||||||
'Bidirectional',
|
|
||||||
Bidirectional.matchingCriteria,
|
|
||||||
Bidirectional.toAnnotation,
|
|
||||||
Bidirectional.toMeasurement
|
|
||||||
);
|
|
||||||
|
|
||||||
MeasurementService.addMapping(
|
|
||||||
csToolsVer4MeasurementSource,
|
|
||||||
'EllipticalRoi',
|
|
||||||
EllipticalRoi.matchingCriteria,
|
|
||||||
EllipticalRoi.toAnnotation,
|
|
||||||
EllipticalRoi.toMeasurement
|
|
||||||
);
|
|
||||||
|
|
||||||
MeasurementService.addMapping(
|
|
||||||
csToolsVer4MeasurementSource,
|
|
||||||
'ArrowAnnotate',
|
|
||||||
ArrowAnnotate.matchingCriteria,
|
|
||||||
ArrowAnnotate.toAnnotation,
|
|
||||||
ArrowAnnotate.toMeasurement
|
|
||||||
);
|
|
||||||
|
|
||||||
return csToolsVer4MeasurementSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _connectToolsToMeasurementService = (
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService
|
|
||||||
) => {
|
|
||||||
const csToolsVer4MeasurementSource = _initMeasurementService(
|
|
||||||
MeasurementService,
|
|
||||||
DisplaySetService
|
|
||||||
);
|
|
||||||
_connectMeasurementServiceToTools(
|
|
||||||
MeasurementService,
|
|
||||||
csToolsVer4MeasurementSource
|
|
||||||
);
|
|
||||||
const { addOrUpdate, remove } = csToolsVer4MeasurementSource;
|
|
||||||
const elementEnabledEvt = cs.EVENTS.ELEMENT_ENABLED;
|
|
||||||
|
|
||||||
/* Measurement Service Events */
|
|
||||||
cs.events.addEventListener(elementEnabledEvt, evt => {
|
|
||||||
// TODO: Debounced update of measurements that are modified
|
|
||||||
function addMeasurement(csToolsEvent) {
|
|
||||||
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const evtDetail = csToolsEvent.detail;
|
|
||||||
const { toolName, toolType, measurementData } = evtDetail;
|
|
||||||
const csToolName = toolName || measurementData.toolType || toolType;
|
|
||||||
|
|
||||||
const measurementId = addOrUpdate(csToolName, evtDetail);
|
|
||||||
|
|
||||||
if (measurementId) {
|
|
||||||
measurementData.id = measurementId;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to add measurement:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMeasurement(csToolsEvent) {
|
|
||||||
try {
|
|
||||||
if (!csToolsEvent.detail.measurementData.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const evtDetail = csToolsEvent.detail;
|
|
||||||
const { toolName, toolType, measurementData } = evtDetail;
|
|
||||||
const csToolName = toolName || measurementData.toolType || toolType;
|
|
||||||
|
|
||||||
evtDetail.id = csToolsEvent.detail.measurementData.id;
|
|
||||||
addOrUpdate(csToolName, evtDetail);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to update measurement:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When csTools fires a removed event, remove the same measurement
|
|
||||||
* from the measurement service
|
|
||||||
*
|
|
||||||
* @param {*} csToolsEvent
|
|
||||||
*/
|
|
||||||
function removeMeasurement(csToolsEvent) {
|
|
||||||
console.log('~~ removeEvt', csToolsEvent);
|
|
||||||
try {
|
|
||||||
if (csToolsEvent.detail.measurementData.id) {
|
|
||||||
remove(csToolsEvent.detail.measurementData.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to remove measurement:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
MEASUREMENTS_CLEARED,
|
|
||||||
MEASUREMENT_UPDATED,
|
|
||||||
} = MeasurementService.EVENTS;
|
|
||||||
|
|
||||||
MeasurementService.subscribe(MEASUREMENTS_CLEARED, () => {
|
|
||||||
globalImageIdSpecificToolStateManager.restoreToolState({});
|
|
||||||
_refreshViewports();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MeasurementService.subscribe(
|
|
||||||
MEASUREMENT_UPDATED,
|
|
||||||
({ source, measurement, notYetUpdatedAtSource }) => {
|
|
||||||
const { id, label } = measurement;
|
|
||||||
|
|
||||||
if (
|
|
||||||
source.name == 'CornerstoneTools' &&
|
|
||||||
notYetUpdatedAtSource === false
|
|
||||||
) {
|
|
||||||
// This event was fired by cornerstone telling the measurement service to sync. Already in sync.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cornerstoneMeasurement = getCornerstoneMeasurementById(id);
|
|
||||||
|
|
||||||
if (cornerstoneMeasurement) {
|
|
||||||
cornerstoneMeasurement.label = label;
|
|
||||||
if (cornerstoneMeasurement.hasOwnProperty('text')) {
|
|
||||||
// Deal with the weird case of ArrowAnnotate.
|
|
||||||
cornerstoneMeasurement.text = label;
|
|
||||||
}
|
|
||||||
|
|
||||||
_refreshViewports();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// on display sets added, check if there are any measurements in measurement service that need to be
|
|
||||||
// put into cornerstone tools tooldata
|
|
||||||
|
|
||||||
const enabledElement = evt.detail.element;
|
|
||||||
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
|
|
||||||
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
|
|
||||||
const removedEvt = csTools.EVENTS.MEASUREMENT_REMOVED;
|
|
||||||
|
|
||||||
enabledElement.addEventListener(completedEvt, addMeasurement);
|
|
||||||
enabledElement.addEventListener(updatedEvt, updateMeasurement);
|
|
||||||
enabledElement.addEventListener(removedEvt, removeMeasurement);
|
|
||||||
});
|
|
||||||
|
|
||||||
return csToolsVer4MeasurementSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _connectMeasurementServiceToTools = (
|
|
||||||
MeasurementService,
|
|
||||||
measurementSource,
|
|
||||||
dataSource
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
MEASUREMENT_REMOVED,
|
|
||||||
RAW_MEASUREMENT_ADDED,
|
|
||||||
} = MeasurementService.EVENTS;
|
|
||||||
const sourceId = measurementSource.id;
|
|
||||||
|
|
||||||
// TODO: This is an unsafe delete
|
|
||||||
// Cornerstone-tools should probably expose a more generic "delete by id"
|
|
||||||
// And have toolState managers expose a method to find any of their toolState by ID
|
|
||||||
// --> csTools.deleteById --> internally checks all registered modules/managers?
|
|
||||||
//
|
|
||||||
// This implementation assumes a single globalImageIdSpecificToolStateManager
|
|
||||||
// It iterates all toolState for all toolTypes, and deletes any with a matching id
|
|
||||||
//
|
|
||||||
// Could potentially use "source" from event to determine tool type and skip some
|
|
||||||
// iterations?
|
|
||||||
|
|
||||||
const {
|
|
||||||
POLYLINE,
|
|
||||||
ELLIPSE,
|
|
||||||
POINT,
|
|
||||||
BIDIRECTIONAL,
|
|
||||||
} = MeasurementService.VALUE_TYPES;
|
|
||||||
|
|
||||||
// TODO -> I get why this was attemped, but its not nearly flexible enough.
|
|
||||||
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
|
||||||
// You can't define a bidirectional tool as a single type..
|
|
||||||
// OHIF-230
|
|
||||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
|
||||||
Length: POLYLINE,
|
|
||||||
EllipticalRoi: ELLIPSE,
|
|
||||||
Bidirectional: BIDIRECTIONAL,
|
|
||||||
ArrowAnnotate: POINT,
|
|
||||||
};
|
|
||||||
|
|
||||||
const VALUE_TYPE_TO_TOOL_TYPE = {
|
|
||||||
[POLYLINE]: 'Length',
|
|
||||||
[ELLIPSE]: 'EllipticalRoi',
|
|
||||||
[BIDIRECTIONAL]: 'Bidirectional',
|
|
||||||
[POINT]: 'ArrowAnnotate',
|
|
||||||
};
|
|
||||||
|
|
||||||
MeasurementService.subscribe(
|
|
||||||
RAW_MEASUREMENT_ADDED,
|
|
||||||
({ source, measurement, data, dataSource }) => {
|
|
||||||
const {
|
|
||||||
referenceStudyUID: StudyInstanceUID,
|
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
} = measurement;
|
|
||||||
|
|
||||||
let toolType;
|
|
||||||
try {
|
|
||||||
toolType = VALUE_TYPE_TO_TOOL_TYPE[measurement.type];
|
|
||||||
} catch {
|
|
||||||
throw Error('Cannot add tool to cornerstone tools');
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageId;
|
|
||||||
if (data.imageId) {
|
|
||||||
// handle dicom json launch, since we cannot create image id from
|
|
||||||
// instance UIDs, each tool should embed the instance metadata
|
|
||||||
// TODO: handle multi instance case
|
|
||||||
imageId = data.imageId;
|
|
||||||
} else {
|
|
||||||
// handle general case of dicom web
|
|
||||||
const instance = {
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
};
|
|
||||||
// TODO: handle multi frame
|
|
||||||
imageId = dataSource.getImageIdsForInstance({ instance });
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
|
||||||
|
|
||||||
if (toolState[imageId] === undefined) {
|
|
||||||
toolState[imageId] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageIdToolState = toolState[imageId];
|
|
||||||
|
|
||||||
// If we don't have tool state for this type of tool, add an empty object
|
|
||||||
if (imageIdToolState[toolType] === undefined) {
|
|
||||||
imageIdToolState[toolType] = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolData = imageIdToolState[toolType];
|
|
||||||
|
|
||||||
toolData.data.push(data);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
MeasurementService.subscribe(
|
|
||||||
MEASUREMENT_REMOVED,
|
|
||||||
({ source, measurement: removedMeasurementId }) => {
|
|
||||||
// THIS POINTS TO ORIGINAL; Not a copy
|
|
||||||
const imageIdSpecificToolState = globalImageIdSpecificToolStateManager.saveToolState();
|
|
||||||
|
|
||||||
// ImageId -->
|
|
||||||
Object.keys(imageIdSpecificToolState).forEach(imageId => {
|
|
||||||
// ImageId --> Tool -->
|
|
||||||
Object.keys(imageIdSpecificToolState[imageId]).forEach(toolName => {
|
|
||||||
const toolState = imageIdSpecificToolState[imageId][toolName];
|
|
||||||
|
|
||||||
let annotationIndex = toolState.data.length - 1;
|
|
||||||
while (annotationIndex >= 0) {
|
|
||||||
const annotation = toolState.data[annotationIndex];
|
|
||||||
|
|
||||||
if (annotation.id === removedMeasurementId) {
|
|
||||||
toolState.data.splice(annotationIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
annotationIndex--;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _getDefaultPosition = event => ({
|
|
||||||
x: (event && event.currentPoints.client.x) || 0,
|
|
||||||
y: (event && event.currentPoints.client.y) || 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function _addConfiguredToolsForElement(
|
|
||||||
UIDialogService,
|
|
||||||
element,
|
|
||||||
tools,
|
|
||||||
configuration
|
|
||||||
) {
|
|
||||||
const internalToolsConfig = _createInternalToolsConfig(UIDialogService);
|
|
||||||
/* Add tools with its custom props through extension configuration. */
|
|
||||||
tools.forEach(tool => {
|
|
||||||
const toolName = new tool().name;
|
|
||||||
const externalToolsConfig = configuration.tools || {};
|
|
||||||
const externalToolProps = externalToolsConfig[toolName] || {};
|
|
||||||
const internalToolProps = internalToolsConfig[toolName] || {};
|
|
||||||
const props = merge(
|
|
||||||
internalToolProps,
|
|
||||||
_parseToolProps(configuration, externalToolProps, tool)
|
|
||||||
);
|
|
||||||
csTools.addToolForElement(element, tool, props);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/* Abstract tools configuration using extension configuration. */
|
|
||||||
function _parseToolProps(configuration, props, tool) {
|
|
||||||
const { annotations } = toolsGroupedByType;
|
|
||||||
// An alternative approach would be to remove the `drawHandlesOnHover` config
|
|
||||||
// from the supported configuration properties in `cornerstone-tools`
|
|
||||||
const toolsWithHideableHandles = annotations.filter(
|
|
||||||
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
let parsedProps = { ...props };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* drawHandles - Never/Always show handles
|
|
||||||
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
|
|
||||||
* hideHandlesIfMoving - Hides the handles whilst you are moving them, for better visibility.
|
|
||||||
*
|
|
||||||
* Does not apply to tools where handles aren't placed in predictable
|
|
||||||
* locations.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
configuration.hideHandles !== false &&
|
|
||||||
toolsWithHideableHandles.includes(tool)
|
|
||||||
) {
|
|
||||||
if (props.configuration) {
|
|
||||||
parsedProps.configuration.drawHandlesOnHover = true;
|
|
||||||
parsedProps.configuration.hideHandlesIfMoving = true;
|
|
||||||
} else {
|
|
||||||
parsedProps.configuration = {
|
|
||||||
drawHandlesOnHover: true,
|
|
||||||
hideHandlesIfMoving: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedProps;
|
eventTarget.addEventListener(
|
||||||
|
EVENTS.ELEMENT_ENABLED,
|
||||||
|
elementEnabledHandler.bind(null)
|
||||||
|
);
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
EVENTS.ELEMENT_DISABLED,
|
||||||
|
elementDisabledHandler.bind(null)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
15
extensions/cornerstone/src/initCineService.ts
Normal file
15
extensions/cornerstone/src/initCineService.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
function initCineService(CineService) {
|
||||||
|
const playClip = (element, playClipOptions) => {
|
||||||
|
return utilities.cine.playClip(element, playClipOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopClip = element => {
|
||||||
|
return utilities.cine.stopClip(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
CineService.setServiceImplementation({ playClip, stopClip });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initCineService;
|
||||||
@ -1,47 +1,79 @@
|
|||||||
import Hammer from 'hammerjs';
|
import {
|
||||||
import cornerstone from 'cornerstone-core';
|
PanTool,
|
||||||
import cornerstoneMath from 'cornerstone-math';
|
WindowLevelTool,
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
StackScrollTool,
|
||||||
import OHIF from '@ohif/core';
|
StackScrollMouseWheelTool,
|
||||||
|
ZoomTool,
|
||||||
|
VolumeRotateMouseWheelTool,
|
||||||
|
MIPJumpToClickTool,
|
||||||
|
LengthTool,
|
||||||
|
RectangleROITool,
|
||||||
|
EllipticalROITool,
|
||||||
|
BidirectionalTool,
|
||||||
|
ArrowAnnotateTool,
|
||||||
|
DragProbeTool,
|
||||||
|
AngleTool,
|
||||||
|
MagnifyTool,
|
||||||
|
CrosshairsTool,
|
||||||
|
SegmentationDisplayTool,
|
||||||
|
init,
|
||||||
|
addTool,
|
||||||
|
annotation,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
const { log } = OHIF;
|
export default function initCornerstoneTools(configuration = {}) {
|
||||||
|
init(configuration);
|
||||||
|
addTool(PanTool);
|
||||||
|
addTool(WindowLevelTool);
|
||||||
|
addTool(StackScrollMouseWheelTool);
|
||||||
|
addTool(StackScrollTool);
|
||||||
|
addTool(ZoomTool);
|
||||||
|
addTool(VolumeRotateMouseWheelTool);
|
||||||
|
addTool(MIPJumpToClickTool);
|
||||||
|
addTool(LengthTool);
|
||||||
|
addTool(RectangleROITool);
|
||||||
|
addTool(EllipticalROITool);
|
||||||
|
addTool(BidirectionalTool);
|
||||||
|
addTool(ArrowAnnotateTool);
|
||||||
|
addTool(DragProbeTool);
|
||||||
|
addTool(AngleTool);
|
||||||
|
addTool(MagnifyTool);
|
||||||
|
addTool(CrosshairsTool);
|
||||||
|
addTool(SegmentationDisplayTool);
|
||||||
|
|
||||||
export default function(configuration = {}) {
|
// Modify annotation tools to use dashed lines on SR
|
||||||
// TODO: Cypress tests are currently grabbing this from the window?
|
const annotationStyle = {
|
||||||
window.cornerstone = cornerstone;
|
textBoxFontSize: '15px',
|
||||||
|
lineWidth: '1.5',
|
||||||
|
};
|
||||||
|
|
||||||
// For debugging
|
const defaultStyles = annotation.config.style.getDefaultToolStyles();
|
||||||
window.cornerstoneTools = cornerstoneTools;
|
annotation.config.style.setDefaultToolStyles({
|
||||||
|
global: {
|
||||||
cornerstoneTools.external.cornerstone = cornerstone;
|
...defaultStyles.global,
|
||||||
cornerstoneTools.external.Hammer = Hammer;
|
...annotationStyle,
|
||||||
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
|
},
|
||||||
cornerstoneTools.init(configuration);
|
|
||||||
|
|
||||||
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(
|
|
||||||
(element, imageId, error) => {
|
|
||||||
log.error(imageId);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the tool font and font size
|
|
||||||
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
|
|
||||||
const fontFamily =
|
|
||||||
'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
|
|
||||||
cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
|
|
||||||
|
|
||||||
// Tool styles/colors
|
|
||||||
cornerstoneTools.toolStyle.setToolWidth(2);
|
|
||||||
cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)');
|
|
||||||
cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
|
|
||||||
|
|
||||||
cornerstoneTools.store.state.touchProximity = 40;
|
|
||||||
|
|
||||||
// Configure stack prefetch
|
|
||||||
cornerstoneTools.stackPrefetch.setConfiguration({
|
|
||||||
maxImagesToPrefetch: Infinity,
|
|
||||||
preserveExistingPool: false,
|
|
||||||
maxSimultaneousRequests: 20,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toolNames = {
|
||||||
|
Pan: PanTool.toolName,
|
||||||
|
ArrowAnnotate: ArrowAnnotateTool.toolName,
|
||||||
|
WindowLevel: WindowLevelTool.toolName,
|
||||||
|
StackScroll: StackScrollTool.toolName,
|
||||||
|
StackScrollMouseWheel: StackScrollMouseWheelTool.toolName,
|
||||||
|
Zoom: ZoomTool.toolName,
|
||||||
|
VolumeRotateMouseWheel: VolumeRotateMouseWheelTool.toolName,
|
||||||
|
MipJumpToClick: MIPJumpToClickTool.toolName,
|
||||||
|
Length: LengthTool.toolName,
|
||||||
|
DragProbe: DragProbeTool.toolName,
|
||||||
|
RectangleROI: RectangleROITool.toolName,
|
||||||
|
EllipticalROI: EllipticalROITool.toolName,
|
||||||
|
Bidirectional: BidirectionalTool.toolName,
|
||||||
|
Angle: AngleTool.toolName,
|
||||||
|
Magnify: MagnifyTool.toolName,
|
||||||
|
Crosshairs: CrosshairsTool.toolName,
|
||||||
|
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { toolNames };
|
||||||
|
|||||||
296
extensions/cornerstone/src/initMeasurementService.js
Normal file
296
extensions/cornerstone/src/initMeasurementService.js
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import { eventTarget } from '@cornerstonejs/core';
|
||||||
|
import { Enums, annotation } from '@cornerstonejs/tools';
|
||||||
|
import { DicomMetadataStore } from '@ohif/core';
|
||||||
|
|
||||||
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
|
|
||||||
|
const { removeAnnotation } = annotation.state;
|
||||||
|
|
||||||
|
const csToolsEvents = Enums.Events;
|
||||||
|
|
||||||
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||||
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||||
|
|
||||||
|
const initMeasurementService = (
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
) => {
|
||||||
|
/* Initialization */
|
||||||
|
const {
|
||||||
|
Length,
|
||||||
|
Bidirectional,
|
||||||
|
EllipticalROI,
|
||||||
|
ArrowAnnotate,
|
||||||
|
} = measurementServiceMappingsFactory(
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
);
|
||||||
|
const csTools3DVer1MeasurementSource = MeasurementService.createSource(
|
||||||
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||||
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Mappings */
|
||||||
|
MeasurementService.addMapping(
|
||||||
|
csTools3DVer1MeasurementSource,
|
||||||
|
'Length',
|
||||||
|
Length.matchingCriteria,
|
||||||
|
Length.toAnnotation,
|
||||||
|
Length.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
MeasurementService.addMapping(
|
||||||
|
csTools3DVer1MeasurementSource,
|
||||||
|
'Bidirectional',
|
||||||
|
Bidirectional.matchingCriteria,
|
||||||
|
Bidirectional.toAnnotation,
|
||||||
|
Bidirectional.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
MeasurementService.addMapping(
|
||||||
|
csTools3DVer1MeasurementSource,
|
||||||
|
'EllipticalROI',
|
||||||
|
EllipticalROI.matchingCriteria,
|
||||||
|
EllipticalROI.toAnnotation,
|
||||||
|
EllipticalROI.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
MeasurementService.addMapping(
|
||||||
|
csTools3DVer1MeasurementSource,
|
||||||
|
'ArrowAnnotate',
|
||||||
|
ArrowAnnotate.matchingCriteria,
|
||||||
|
ArrowAnnotate.toAnnotation,
|
||||||
|
ArrowAnnotate.toMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
return csTools3DVer1MeasurementSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectToolsToMeasurementService = (
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
) => {
|
||||||
|
const csTools3DVer1MeasurementSource = initMeasurementService(
|
||||||
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
);
|
||||||
|
connectMeasurementServiceToTools(
|
||||||
|
MeasurementService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
csTools3DVer1MeasurementSource
|
||||||
|
);
|
||||||
|
const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource;
|
||||||
|
|
||||||
|
//
|
||||||
|
function addMeasurement(csToolsEvent) {
|
||||||
|
try {
|
||||||
|
const annotationAddedEventDetail = csToolsEvent.detail;
|
||||||
|
const {
|
||||||
|
annotation: { metadata, annotationUID },
|
||||||
|
} = annotationAddedEventDetail;
|
||||||
|
const { toolName } = metadata;
|
||||||
|
|
||||||
|
// To force the measurementUID be the same as the annotationUID
|
||||||
|
// Todo: this should be changed when a measurement can include multiple annotations
|
||||||
|
// in the future
|
||||||
|
annotationAddedEventDetail.uid = annotationUID;
|
||||||
|
annotationToMeasurement(toolName, annotationAddedEventDetail);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to update measurement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function updateMeasurement(csToolsEvent) {
|
||||||
|
try {
|
||||||
|
const annotationModifiedEventDetail = csToolsEvent.detail;
|
||||||
|
|
||||||
|
const {
|
||||||
|
annotation: { metadata, annotationUID },
|
||||||
|
} = annotationModifiedEventDetail;
|
||||||
|
|
||||||
|
// If the measurement hasn't been added, don't modify it
|
||||||
|
const measurement = MeasurementService.getMeasurement(annotationUID);
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { toolName } = metadata;
|
||||||
|
|
||||||
|
annotationModifiedEventDetail.uid = annotationUID;
|
||||||
|
annotationToMeasurement(toolName, annotationModifiedEventDetail);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to update measurement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When csTools fires a removed event, remove the same measurement
|
||||||
|
* from the measurement service
|
||||||
|
*
|
||||||
|
* @param {*} csToolsEvent
|
||||||
|
*/
|
||||||
|
function removeMeasurement(csToolsEvent) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const annotationRemovedEventDetail = csToolsEvent.detail;
|
||||||
|
const {
|
||||||
|
annotation: { annotationUID },
|
||||||
|
} = annotationRemovedEventDetail;
|
||||||
|
|
||||||
|
const measurement = MeasurementService.getMeasurement(annotationUID);
|
||||||
|
|
||||||
|
if (measurement) {
|
||||||
|
console.log('~~ removeEvt', csToolsEvent);
|
||||||
|
remove(annotationUID, annotationRemovedEventDetail);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to update measurement:', error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to remove measurement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on display sets added, check if there are any measurements in measurement service that need to be
|
||||||
|
// put into cornerstone tools
|
||||||
|
const completedEvt = csToolsEvents.ANNOTATION_COMPLETED;
|
||||||
|
const updatedEvt = csToolsEvents.ANNOTATION_MODIFIED;
|
||||||
|
const removedEvt = csToolsEvents.ANNOTATION_REMOVED;
|
||||||
|
|
||||||
|
eventTarget.addEventListener(completedEvt, addMeasurement);
|
||||||
|
eventTarget.addEventListener(updatedEvt, updateMeasurement);
|
||||||
|
eventTarget.addEventListener(removedEvt, removeMeasurement);
|
||||||
|
|
||||||
|
return csTools3DVer1MeasurementSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectMeasurementServiceToTools = (
|
||||||
|
MeasurementService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
measurementSource
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
MEASUREMENT_REMOVED,
|
||||||
|
MEASUREMENTS_CLEARED,
|
||||||
|
MEASUREMENT_UPDATED,
|
||||||
|
RAW_MEASUREMENT_ADDED,
|
||||||
|
} = MeasurementService.EVENTS;
|
||||||
|
|
||||||
|
const csTools3DVer1MeasurementSource = MeasurementService.getSource(
|
||||||
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||||
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
MeasurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => {
|
||||||
|
if (!Object.keys(measurements).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const measurement of Object.values(measurements)) {
|
||||||
|
const { uid, source } = measurement;
|
||||||
|
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAnnotation(uid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MeasurementService.subscribe(
|
||||||
|
MEASUREMENT_UPDATED,
|
||||||
|
({ source, measurement, notYetUpdatedAtSource }) => {
|
||||||
|
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notYetUpdatedAtSource === false) {
|
||||||
|
// This event was fired by cornerstone telling the measurement service to sync.
|
||||||
|
// Already in sync.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { uid, label } = measurement;
|
||||||
|
|
||||||
|
const sourceAnnotation = annotation.state.getAnnotation(uid);
|
||||||
|
const { data, metadata } = sourceAnnotation;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.label !== label) {
|
||||||
|
data.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.toolName === 'ArrowAnnotate') {
|
||||||
|
data.text = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: trigger render for annotation
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MeasurementService.subscribe(
|
||||||
|
RAW_MEASUREMENT_ADDED,
|
||||||
|
({ source, measurement, data, dataSource }) => {
|
||||||
|
if (source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
referenceSeriesUID,
|
||||||
|
referenceStudyUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
} = measurement;
|
||||||
|
|
||||||
|
const instance = DicomMetadataStore.getInstance(
|
||||||
|
referenceStudyUID,
|
||||||
|
referenceSeriesUID,
|
||||||
|
SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageId = dataSource.getImageIdsForInstance({ instance });
|
||||||
|
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
||||||
|
annotationManager.addAnnotation({
|
||||||
|
annotationUID: measurement.uid,
|
||||||
|
highlighted: false,
|
||||||
|
isLocked: false,
|
||||||
|
invalidated: false,
|
||||||
|
metadata: {
|
||||||
|
toolName: measurement.toolName,
|
||||||
|
FrameOfReferenceUID: measurement.FrameOfReferenceUID,
|
||||||
|
referencedImageId: imageId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
text: data.annotation.data.text,
|
||||||
|
handles: { ...data.annotation.data.handles },
|
||||||
|
cachedStats: { ...data.annotation.data.cachedStats },
|
||||||
|
label: data.annotation.data.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MeasurementService.subscribe(
|
||||||
|
MEASUREMENT_REMOVED,
|
||||||
|
({ source, measurement: removedMeasurementId }) => {
|
||||||
|
if (source?.name && source.name !== CORNERSTONE_3D_TOOLS_SOURCE_NAME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeAnnotation(removedMeasurementId);
|
||||||
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||||
|
// Note: We could do a better job by triggering the render on the
|
||||||
|
// viewport itself, but the removeAnnotation does not include that info...
|
||||||
|
renderingEngine.render();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
initMeasurementService,
|
||||||
|
connectToolsToMeasurementService,
|
||||||
|
connectMeasurementServiceToTools,
|
||||||
|
};
|
||||||
141
extensions/cornerstone/src/initSegmentationService.js
Normal file
141
extensions/cornerstone/src/initSegmentationService.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { eventTarget, cache, triggerEvent } from '@cornerstonejs/core';
|
||||||
|
import * as csTools from '@cornerstonejs/tools';
|
||||||
|
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
|
||||||
|
import Labelmap from './utils/segmentationServiceMappings/Labelmap';
|
||||||
|
|
||||||
|
function initSegmentationService(
|
||||||
|
SegmentationService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
) {
|
||||||
|
connectToolsToSegmentationService(
|
||||||
|
SegmentationService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
);
|
||||||
|
|
||||||
|
connectSegmentationServiceToTools(
|
||||||
|
SegmentationService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectToolsToSegmentationService(
|
||||||
|
SegmentationService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
) {
|
||||||
|
connectSegmentationServiceToTools(
|
||||||
|
SegmentationService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
);
|
||||||
|
const segmentationUpdated = csToolsEnums.Events.SEGMENTATION_MODIFIED;
|
||||||
|
|
||||||
|
eventTarget.addEventListener(segmentationUpdated, evt => {
|
||||||
|
const { segmentationId } = evt.detail;
|
||||||
|
const segmentationState = csTools.segmentation.state.getSegmentation(
|
||||||
|
segmentationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!segmentationState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Object.keys(segmentationState.representationData).includes(
|
||||||
|
csToolsEnums.SegmentationRepresentations.Labelmap
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error('Non-labelmap representations are not supported yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: handle other representations when available in cornerstone3D
|
||||||
|
const segmentationSchema = Labelmap.toSegmentation(segmentationState);
|
||||||
|
|
||||||
|
try {
|
||||||
|
SegmentationService.addOrUpdateSegmentation(
|
||||||
|
segmentationId,
|
||||||
|
segmentationSchema
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to add/update segmentation ${segmentationId}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectSegmentationServiceToTools(
|
||||||
|
SegmentationService,
|
||||||
|
CornerstoneViewportService
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
SEGMENTATION_UPDATED,
|
||||||
|
SEGMENTATION_REMOVED,
|
||||||
|
} = SegmentationService.EVENTS;
|
||||||
|
|
||||||
|
SegmentationService.subscribe(SEGMENTATION_REMOVED, ({ id }) => {
|
||||||
|
// Todo: This should be from the configuration
|
||||||
|
const removeFromCache = true;
|
||||||
|
|
||||||
|
const sourceSegState = csTools.segmentation.state.getSegmentation(id);
|
||||||
|
|
||||||
|
if (!sourceSegState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolGroupIds = csTools.segmentation.state.getToolGroupsWithSegmentation(
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
|
||||||
|
toolGroupId
|
||||||
|
);
|
||||||
|
|
||||||
|
const UIDsToRemove = [];
|
||||||
|
segmentationRepresentations.forEach(representation => {
|
||||||
|
if (representation.segmentationId === id) {
|
||||||
|
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
csTools.segmentation.removeSegmentationsFromToolGroup(
|
||||||
|
toolGroupId,
|
||||||
|
UIDsToRemove
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// cleanup the segmentation state too
|
||||||
|
csTools.segmentation.state.removeSegmentation(id);
|
||||||
|
|
||||||
|
if (removeFromCache) {
|
||||||
|
cache.removeVolumeLoadObject(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SegmentationService.subscribe(
|
||||||
|
SEGMENTATION_UPDATED,
|
||||||
|
({ id, segmentation, notYetUpdatedAtSource }) => {
|
||||||
|
if (notYetUpdatedAtSource === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { label, text } = segmentation;
|
||||||
|
|
||||||
|
const sourceSegmentation = csTools.segmentation.state.getSegmentation(id);
|
||||||
|
|
||||||
|
// Update the label in the source if necessary
|
||||||
|
if (sourceSegmentation.label !== label) {
|
||||||
|
sourceSegmentation.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceSegmentation.text !== text) {
|
||||||
|
sourceSegmentation.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerEvent(eventTarget, csTools.Enums.Events.SEGMENTATION_MODIFIED, {
|
||||||
|
segmentationId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default initSegmentationService;
|
||||||
@ -1,13 +1,20 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
|
import { volumeLoader } from '@cornerstonejs/core';
|
||||||
|
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
import { errorHandler } from '@ohif/core';
|
import { errorHandler } from '@ohif/core';
|
||||||
|
|
||||||
|
const { registerVolumeLoader } = volumeLoader;
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
function initWebWorkers() {
|
function initWebWorkers(appConfig) {
|
||||||
const config = {
|
const config = {
|
||||||
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
|
maxWebWorkers: Math.min(
|
||||||
|
Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||||
|
appConfig.maxNumberOfWebWorkers
|
||||||
|
),
|
||||||
startWebWorkersOnDemand: true,
|
startWebWorkersOnDemand: true,
|
||||||
taskConfiguration: {
|
taskConfiguration: {
|
||||||
decodeTask: {
|
decodeTask: {
|
||||||
@ -24,11 +31,27 @@ function initWebWorkers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function initWADOImageLoader(UserAuthenticationService) {
|
export default function initWADOImageLoader(
|
||||||
|
UserAuthenticationService,
|
||||||
|
appConfig
|
||||||
|
) {
|
||||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||||
|
|
||||||
|
registerVolumeLoader(
|
||||||
|
'cornerstoneStreamingImageVolume',
|
||||||
|
cornerstoneStreamingImageVolumeLoader
|
||||||
|
);
|
||||||
|
|
||||||
cornerstoneWADOImageLoader.configure({
|
cornerstoneWADOImageLoader.configure({
|
||||||
|
decodeConfig: {
|
||||||
|
// !! IMPORTANT !!
|
||||||
|
// We should set this flag to false, since, by default cornerstone-wado-image-loader
|
||||||
|
// will convert everything to integers (to be able to work with cornerstone-2d).
|
||||||
|
// Until the default is set to true (which is the case for cornerstone3D),
|
||||||
|
// we should set this flag to false.
|
||||||
|
convertFloatPixelDataToInt: false,
|
||||||
|
},
|
||||||
beforeSend: function(xhr) {
|
beforeSend: function(xhr) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
|
||||||
@ -38,10 +61,7 @@ export default function initWADOImageLoader(UserAuthenticationService) {
|
|||||||
// For now we use image/jls and image/x-jls because some servers still use the old type
|
// For now we use image/jls and image/x-jls because some servers still use the old type
|
||||||
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
|
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
|
||||||
const xhrRequestHeaders = {
|
const xhrRequestHeaders = {
|
||||||
// To prevent Preflight requests:
|
|
||||||
accept: 'multipart/related; type=application/octet-stream',
|
accept: 'multipart/related; type=application/octet-stream',
|
||||||
//
|
|
||||||
//accept: 'multipart/related; type="image/x-jls"',
|
|
||||||
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
|
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,5 +76,5 @@ export default function initWADOImageLoader(UserAuthenticationService) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
initWebWorkers();
|
initWebWorkers(appConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
import { synchronizers, SynchronizerManager } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
import { pubSubServiceInterface } from '@ohif/core';
|
||||||
|
|
||||||
|
const EVENTS = {
|
||||||
|
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SyncGroup = {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
source: boolean;
|
||||||
|
target: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const POSITION = 'cameraposition';
|
||||||
|
const VOI = 'voi';
|
||||||
|
|
||||||
|
export default class SyncGroupService {
|
||||||
|
serviceManager: any;
|
||||||
|
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||||
|
EVENTS: { [key: string]: string };
|
||||||
|
|
||||||
|
constructor(serviceManager) {
|
||||||
|
this.serviceManager = serviceManager;
|
||||||
|
this.listeners = {};
|
||||||
|
this.EVENTS = EVENTS;
|
||||||
|
//
|
||||||
|
Object.assign(this, pubSubServiceInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSynchronizer(type: string, id: string) {
|
||||||
|
type = type.toLowerCase();
|
||||||
|
if (type === POSITION) {
|
||||||
|
return synchronizers.createCameraPositionSynchronizer(id);
|
||||||
|
} else if (type === VOI) {
|
||||||
|
return synchronizers.createVOISynchronizer(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addViewportToSyncGroup(
|
||||||
|
viewportId: string,
|
||||||
|
renderingEngineId: string,
|
||||||
|
syncGroups?: SyncGroup[]
|
||||||
|
): void {
|
||||||
|
if (!syncGroups || !syncGroups.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncGroups.forEach(syncGroup => {
|
||||||
|
const { type, id, target, source } = syncGroup;
|
||||||
|
|
||||||
|
let synchronizer = SynchronizerManager.getSynchronizer(id);
|
||||||
|
|
||||||
|
if (!synchronizer) {
|
||||||
|
synchronizer = this._createSynchronizer(type, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target && source) {
|
||||||
|
synchronizer.add({
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
} else if (source) {
|
||||||
|
synchronizer.addSource({
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
});
|
||||||
|
} else if (target) {
|
||||||
|
synchronizer.addTarget({
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
SynchronizerManager.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeViewportFromSyncGroup(
|
||||||
|
viewportId: string,
|
||||||
|
renderingEngineId: string
|
||||||
|
): void {
|
||||||
|
const synchronizers = SynchronizerManager.getAllSynchronizers();
|
||||||
|
|
||||||
|
synchronizers.forEach(synchronizer => {
|
||||||
|
if (!synchronizer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronizer.remove({
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if any viewport is left in any of the sync groups, if not, delete that sync group
|
||||||
|
const sourceViewports = synchronizer.getSourceViewports();
|
||||||
|
const targetViewports = synchronizer.getTargetViewports();
|
||||||
|
|
||||||
|
if (!sourceViewports.length && !targetViewports.length) {
|
||||||
|
SynchronizerManager.destroySynchronizer(synchronizer.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import SyncGroupService from './SyncGroupService';
|
||||||
|
|
||||||
|
export default function ExtendedSyncGroupService(serviceManager) {
|
||||||
|
return {
|
||||||
|
name: 'SyncGroupService',
|
||||||
|
create: ({ configuration = {} }) => {
|
||||||
|
return new SyncGroupService(serviceManager);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,260 @@
|
|||||||
|
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
import { pubSubServiceInterface } from '@ohif/core';
|
||||||
|
|
||||||
|
const EVENTS = {
|
||||||
|
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||||
|
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
|
||||||
|
};
|
||||||
|
|
||||||
|
type Tool = {
|
||||||
|
toolName: string;
|
||||||
|
bindings?: typeof Enums.MouseBindings | Enums.KeyboardBindings;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Tools = {
|
||||||
|
active: Tool[];
|
||||||
|
passive?: Tool[];
|
||||||
|
enabled?: Tool[];
|
||||||
|
disabled?: Tool[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class ToolGroupService {
|
||||||
|
serviceManager: any;
|
||||||
|
private toolGroupIds: Set<string> = new Set();
|
||||||
|
/**
|
||||||
|
* Service-specific
|
||||||
|
*/
|
||||||
|
listeners: { [key: string]: Function[] };
|
||||||
|
EVENTS: { [key: string]: string };
|
||||||
|
|
||||||
|
constructor(serviceManager) {
|
||||||
|
const { CornerstoneViewportService } = serviceManager.services;
|
||||||
|
this.CornerstoneViewportService = CornerstoneViewportService;
|
||||||
|
this.listeners = {};
|
||||||
|
this.EVENTS = EVENTS;
|
||||||
|
Object.assign(this, pubSubServiceInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the cornerstone ToolGroup for a given toolGroup UID
|
||||||
|
* @param {string} toolGroupId - The toolGroup uid
|
||||||
|
* @returns {IToolGroup} - The toolGroup
|
||||||
|
*/
|
||||||
|
public getToolGroup(toolGroupId: string): Types.IToolGroup | void {
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||||
|
return toolGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getToolGroupIds(): string[] {
|
||||||
|
return Array.from(this.toolGroupIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getToolGroupForViewport(viewportId: string): Types.IToolGroup | void {
|
||||||
|
const renderingEngine = this.CornerstoneViewportService.getRenderingEngine();
|
||||||
|
return ToolGroupManager.getToolGroupForViewport(
|
||||||
|
viewportId,
|
||||||
|
renderingEngine.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveToolForViewport(viewportId: string): string {
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId);
|
||||||
|
if (!toolGroup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolGroup.getActivePrimaryMouseButtonTool();
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
ToolGroupManager.destroy();
|
||||||
|
this.toolGroupIds = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
public disable(viewportId: string, renderingEngineId: string): void {
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!toolGroup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
|
||||||
|
const viewportIds = toolGroup.getViewportIds();
|
||||||
|
// if (viewportIds.length === 0) {
|
||||||
|
// ToolGroupManager.destroyToolGroup(toolGroup.id);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
public addViewportToToolGroup(
|
||||||
|
viewportId: string,
|
||||||
|
renderingEngineId: string,
|
||||||
|
toolGroupId?: string
|
||||||
|
): void {
|
||||||
|
if (!toolGroupId) {
|
||||||
|
// If toolGroupId is not provided, add the viewport to all toolGroups
|
||||||
|
const toolGroups = ToolGroupManager.getAllToolGroups();
|
||||||
|
toolGroups.forEach(toolGroup => {
|
||||||
|
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||||
|
if (!toolGroup) {
|
||||||
|
toolGroup = this.createToolGroup(toolGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
|
||||||
|
}
|
||||||
|
|
||||||
|
public createToolGroup(toolGroupId: string): Types.IToolGroup {
|
||||||
|
if (this.getToolGroup(toolGroupId)) {
|
||||||
|
throw new Error(`ToolGroup ${toolGroupId} already exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the toolGroup doesn't exist, create it
|
||||||
|
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
|
||||||
|
this.toolGroupIds.add(toolGroupId);
|
||||||
|
|
||||||
|
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
|
||||||
|
|
||||||
|
return toolGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addToolsToToolGroup(
|
||||||
|
toolGroupId: string,
|
||||||
|
tools: Array<Tool>,
|
||||||
|
configs: any = {}
|
||||||
|
): void {
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||||
|
// this.changeConfigurationIfNecessary(toolGroup, volumeId);
|
||||||
|
this._addTools(toolGroup, tools, configs);
|
||||||
|
this._setToolsMode(toolGroup, tools);
|
||||||
|
}
|
||||||
|
|
||||||
|
public createToolGroupAndAddTools(
|
||||||
|
toolGroupId: string,
|
||||||
|
tools: Array<Tool>,
|
||||||
|
configs: any = {}
|
||||||
|
): Types.IToolGroup {
|
||||||
|
const toolGroup = this.createToolGroup(toolGroupId);
|
||||||
|
this.addToolsToToolGroup(toolGroupId, tools, configs);
|
||||||
|
return toolGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
private changeConfigurationIfNecessary(toolGroup, volumeUID) {
|
||||||
|
// handle specific assignment for volumeUID (e.g., fusion)
|
||||||
|
const toolInstances = toolGroup._toolInstances;
|
||||||
|
// Object.values(toolInstances).forEach(toolInstance => {
|
||||||
|
// if (toolInstance.configuration) {
|
||||||
|
// toolInstance.configuration.volumeUID = volumeUID;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tool's configuration based on the tool name and tool group id
|
||||||
|
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
|
||||||
|
* @param toolName - The name of the tool
|
||||||
|
* @returns The configuration of the tool.
|
||||||
|
*/
|
||||||
|
public getToolConfiguration(toolGroupId: string, toolName: string) {
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||||
|
if (!toolGroup) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tool = toolGroup.getToolInstance(toolName);
|
||||||
|
if (!tool) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tool.configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the tool instance configuration. This will update the tool instance configuration
|
||||||
|
* on the toolGroup
|
||||||
|
* @param toolGroupId - The id of the tool group that the tool instance belongs to.
|
||||||
|
* @param toolName - The name of the tool
|
||||||
|
* @param config - The configuration object that you want to set.
|
||||||
|
*/
|
||||||
|
public setToolConfiguration(toolGroupId, toolName, config) {
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroup(toolGroupId);
|
||||||
|
const toolInstance = toolGroup.getToolInstance(toolName);
|
||||||
|
toolInstance.configuration = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getToolNames(toolGroupTools: Tools): string[] {
|
||||||
|
const toolNames = [];
|
||||||
|
toolGroupTools.active.forEach(tool => {
|
||||||
|
toolNames.push(tool.toolName);
|
||||||
|
});
|
||||||
|
if (toolGroupTools.passive) {
|
||||||
|
toolGroupTools.passive.forEach(tool => {
|
||||||
|
toolNames.push(tool.toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolGroupTools.enabled) {
|
||||||
|
toolGroupTools.enabled.forEach(tool => {
|
||||||
|
toolNames.push(tool.toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolGroupTools.disabled) {
|
||||||
|
toolGroupTools.disabled.forEach(tool => {
|
||||||
|
toolNames.push(tool.toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setToolsMode(toolGroup, tools) {
|
||||||
|
const { active, passive, enabled, disabled } = tools;
|
||||||
|
active.forEach(({ toolName, bindings }) => {
|
||||||
|
toolGroup.setToolActive(toolName, { bindings });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (passive) {
|
||||||
|
passive.forEach(({ toolName }) => {
|
||||||
|
toolGroup.setToolPassive(toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
enabled.forEach(({ toolName }) => {
|
||||||
|
toolGroup.setToolEnabled(toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
disabled.forEach(({ toolName }) => {
|
||||||
|
toolGroup.setToolDisabled(toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addTools(toolGroup, tools, configs) {
|
||||||
|
const toolNames = this._getToolNames(tools);
|
||||||
|
toolNames.forEach(toolName => {
|
||||||
|
// Initialize the toolConfig if no configuration is provided
|
||||||
|
const toolConfig = configs[toolName] ?? {};
|
||||||
|
|
||||||
|
// if (volumeUID) {
|
||||||
|
// toolConfig.volumeUID = volumeUID;
|
||||||
|
// }
|
||||||
|
|
||||||
|
toolGroup.addTool(toolName, { ...toolConfig });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import ToolGroupService from './ToolGroupService';
|
||||||
|
|
||||||
|
export default function ExtendedToolGroupService(serviceManager) {
|
||||||
|
return {
|
||||||
|
name: 'ToolGroupService',
|
||||||
|
create: ({ configuration = {} }) => {
|
||||||
|
return new ToolGroupService(serviceManager);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,227 @@
|
|||||||
|
import {
|
||||||
|
cache as cs3DCache,
|
||||||
|
Enums,
|
||||||
|
Types,
|
||||||
|
volumeLoader,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
import { utils, pubSubServiceInterface } from '@ohif/core';
|
||||||
|
|
||||||
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
|
|
||||||
|
export type StackData = {
|
||||||
|
StudyInstanceUID: string;
|
||||||
|
displaySetInstanceUID: string;
|
||||||
|
imageIds: string[];
|
||||||
|
frameRate?: number;
|
||||||
|
isClip?: boolean;
|
||||||
|
initialImageIndex?: number | string | null;
|
||||||
|
viewportType: Enums.ViewportType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VolumeData = {
|
||||||
|
StudyInstanceUID: string;
|
||||||
|
displaySetInstanceUIDs: string[]; // can have more than one displaySet (fusion)
|
||||||
|
imageIds: string[][]; // can have more than one imageId list (fusion)
|
||||||
|
volumes: Types.IVolume[];
|
||||||
|
viewportType: Enums.ViewportType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VOLUME_LOADER_SCHEME = 'streaming-wadors';
|
||||||
|
|
||||||
|
const EVENTS = {
|
||||||
|
VIEWPORT_DATA_CHANGED: 'event::cornerstone::viewportdatachanged',
|
||||||
|
};
|
||||||
|
|
||||||
|
class CornerstoneCacheService {
|
||||||
|
stackImageIds: Map<string, string[]> = new Map();
|
||||||
|
volumeImageIds: Map<string, string[]> = new Map();
|
||||||
|
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||||
|
EVENTS: { [key: string]: string };
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.listeners = {};
|
||||||
|
this.EVENTS = EVENTS;
|
||||||
|
Object.assign(this, pubSubServiceInterface);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCacheSize() {
|
||||||
|
return cs3DCache.getCacheSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCacheFreeSpace() {
|
||||||
|
return cs3DCache.getBytesAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getViewportData(
|
||||||
|
viewportIndex: number,
|
||||||
|
displaySets: unknown[],
|
||||||
|
viewportType: string,
|
||||||
|
dataSource: unknown,
|
||||||
|
initialImageIndex?: number
|
||||||
|
): Promise<StackData | VolumeData> {
|
||||||
|
const cs3DViewportType = getCornerstoneViewportType(viewportType);
|
||||||
|
let viewportData: StackData | VolumeData;
|
||||||
|
|
||||||
|
if (cs3DViewportType === Enums.ViewportType.STACK) {
|
||||||
|
viewportData = await this._getStackViewportData(
|
||||||
|
dataSource,
|
||||||
|
displaySets,
|
||||||
|
initialImageIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
|
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportData.viewportType = cs3DViewportType;
|
||||||
|
|
||||||
|
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||||
|
viewportData,
|
||||||
|
viewportIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return viewportData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async invalidateViewportData(
|
||||||
|
viewportData: VolumeData,
|
||||||
|
invalidatedDisplaySetInstanceUID: string,
|
||||||
|
dataSource,
|
||||||
|
DisplaySetService
|
||||||
|
) {
|
||||||
|
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||||
|
throw new Error('Invalidation of StackViewport is not supported yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeId = invalidatedDisplaySetInstanceUID;
|
||||||
|
const volume = cs3DCache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (volume) {
|
||||||
|
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySets = viewportData.displaySetInstanceUIDs.map(
|
||||||
|
DisplaySetService.getDisplaySetByUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const newViewportData = await this._getVolumeViewportData(
|
||||||
|
dataSource,
|
||||||
|
displaySets
|
||||||
|
);
|
||||||
|
|
||||||
|
return newViewportData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getStackViewportData(
|
||||||
|
dataSource,
|
||||||
|
displaySets,
|
||||||
|
initialImageIndex
|
||||||
|
): StackData {
|
||||||
|
// For Stack Viewport we don't have fusion currently
|
||||||
|
const displaySet = displaySets[0];
|
||||||
|
|
||||||
|
let stackImageIds = this.stackImageIds.get(
|
||||||
|
displaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!stackImageIds) {
|
||||||
|
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||||
|
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||||
|
|
||||||
|
const stackData: StackData = {
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
viewportType: Enums.ViewportType.STACK,
|
||||||
|
imageIds: stackImageIds,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof initialImageIndex === 'number') {
|
||||||
|
stackData.initialImageIndex = initialImageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stackData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getVolumeViewportData(
|
||||||
|
dataSource,
|
||||||
|
displaySets
|
||||||
|
): Promise<VolumeData> {
|
||||||
|
// Check the cache for multiple scenarios to see if we need to
|
||||||
|
// decache the volume data from other viewports or not
|
||||||
|
|
||||||
|
const volumeImageIdsArray = [];
|
||||||
|
const volumes = [];
|
||||||
|
|
||||||
|
for (const displaySet of displaySets) {
|
||||||
|
const volumeId = displaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
|
let volumeImageIds = this.volumeImageIds.get(
|
||||||
|
displaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let volume = cs3DCache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (!volumeImageIds || !volume) {
|
||||||
|
volumeImageIds = this._getCornerstoneVolumeImageIds(
|
||||||
|
displaySet,
|
||||||
|
dataSource
|
||||||
|
);
|
||||||
|
|
||||||
|
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||||
|
imageIds: volumeImageIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.volumeImageIds.set(
|
||||||
|
displaySet.displaySetInstanceUID,
|
||||||
|
volumeImageIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeImageIdsArray.push(volumeImageIds);
|
||||||
|
volumes.push(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert displaySets are from the same study
|
||||||
|
const { StudyInstanceUID } = displaySets[0];
|
||||||
|
const displaySetInstanceUIDs = [];
|
||||||
|
|
||||||
|
displaySets.forEach(displaySet => {
|
||||||
|
if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
|
||||||
|
throw new Error('Display sets are not from the same study');
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
StudyInstanceUID,
|
||||||
|
displaySetInstanceUIDs,
|
||||||
|
imageIds: volumeImageIdsArray,
|
||||||
|
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||||
|
volumes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
||||||
|
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||||
|
const stackImageIds = this._getCornerstoneStackImageIds(
|
||||||
|
displaySet,
|
||||||
|
dataSource
|
||||||
|
);
|
||||||
|
|
||||||
|
return stackImageIds.map(imageId => {
|
||||||
|
const imageURI = utils.imageIdToURI(imageId);
|
||||||
|
return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CacheService = new CornerstoneCacheService();
|
||||||
|
export default CacheService;
|
||||||
@ -0,0 +1,579 @@
|
|||||||
|
import { pubSubServiceInterface } from '@ohif/core';
|
||||||
|
import {
|
||||||
|
RenderingEngine,
|
||||||
|
StackViewport,
|
||||||
|
Enums,
|
||||||
|
Types,
|
||||||
|
getRenderingEngine,
|
||||||
|
utilities as csUtils,
|
||||||
|
VolumeViewport,
|
||||||
|
cache,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
import { utilities as csToolsUtils } from '@cornerstonejs/tools';
|
||||||
|
import { IViewportService } from './IViewportService';
|
||||||
|
import { RENDERING_ENGINE_ID } from './constants';
|
||||||
|
import ViewportInfo, {
|
||||||
|
ViewportOptions,
|
||||||
|
DisplaySetOptions,
|
||||||
|
PublicViewportOptions,
|
||||||
|
} from './Viewport';
|
||||||
|
import { StackData, VolumeData } from './CornerstoneCacheService';
|
||||||
|
import {
|
||||||
|
setColormap,
|
||||||
|
setLowerUpperColorTransferFunction,
|
||||||
|
} from '../../utils/colormap/transferFunctionHelpers';
|
||||||
|
|
||||||
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
|
|
||||||
|
const EVENTS = {
|
||||||
|
VIEWPORT_INFO_CREATED:
|
||||||
|
'event::cornerstone::viewportservice:viewportinfocreated',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles cornerstone viewport logic including enabling, disabling, and
|
||||||
|
* updating the viewport.
|
||||||
|
*/
|
||||||
|
class CornerstoneViewportService implements IViewportService {
|
||||||
|
renderingEngine: Types.IRenderingEngine | null;
|
||||||
|
viewportsInfo: Map<number, ViewportInfo>;
|
||||||
|
viewportGridResizeObserver: ResizeObserver | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service-specific
|
||||||
|
*/
|
||||||
|
EVENTS: { [key: string]: string };
|
||||||
|
listeners: { [key: string]: Array<(...args: any[]) => void> };
|
||||||
|
_broadcastEvent: unknown; // we should be able to extend the PubSub class to get this
|
||||||
|
// Some configs
|
||||||
|
enableResizeDetector: true;
|
||||||
|
resizeRefreshRateMs: 200;
|
||||||
|
resizeRefreshMode: 'debounce';
|
||||||
|
|
||||||
|
constructor(servicesManager) {
|
||||||
|
this.renderingEngine = null;
|
||||||
|
this.viewportGridResizeObserver = null;
|
||||||
|
this.viewportsInfo = new Map();
|
||||||
|
//
|
||||||
|
this.listeners = {};
|
||||||
|
this.EVENTS = EVENTS;
|
||||||
|
const { HangingProtocolService } = servicesManager.services;
|
||||||
|
this.HangingProtocolService = HangingProtocolService;
|
||||||
|
Object.assign(this, pubSubServiceInterface);
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the HTML element to the viewportService
|
||||||
|
* @param {*} viewportIndex
|
||||||
|
* @param {*} elementRef
|
||||||
|
*/
|
||||||
|
public enableElement(
|
||||||
|
viewportIndex: number,
|
||||||
|
viewportOptions: PublicViewportOptions,
|
||||||
|
elementRef: HTMLDivElement
|
||||||
|
) {
|
||||||
|
const viewportId =
|
||||||
|
viewportOptions.viewportId || this.getViewportId(viewportIndex);
|
||||||
|
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
|
||||||
|
viewportInfo.setElement(elementRef);
|
||||||
|
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportId(viewportIndex: number): string {
|
||||||
|
return `viewport-${viewportIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It retrieves the renderingEngine if it does exist, or creates one otherwise
|
||||||
|
* @returns {RenderingEngine} rendering engine
|
||||||
|
*/
|
||||||
|
public getRenderingEngine() {
|
||||||
|
// get renderingEngine from cache if it exists
|
||||||
|
const renderingEngine = getRenderingEngine(RENDERING_ENGINE_ID);
|
||||||
|
|
||||||
|
if (renderingEngine) {
|
||||||
|
this.renderingEngine = renderingEngine;
|
||||||
|
return this.renderingEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!renderingEngine || renderingEngine.hasBeenDestroyed) {
|
||||||
|
this.renderingEngine = new RenderingEngine(RENDERING_ENGINE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.renderingEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It triggers the resize on the rendering engine.
|
||||||
|
*/
|
||||||
|
public resize() {
|
||||||
|
const immediate = true;
|
||||||
|
const resetPan = false;
|
||||||
|
const resetZoom = false;
|
||||||
|
|
||||||
|
this.renderingEngine.resize(immediate, resetPan, resetZoom);
|
||||||
|
this.renderingEngine.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the viewport from cornerstone, and destroys the rendering engine
|
||||||
|
*/
|
||||||
|
public destroy() {
|
||||||
|
this._removeResizeObserver();
|
||||||
|
this.viewportGridResizeObserver = null;
|
||||||
|
this.renderingEngine.destroy();
|
||||||
|
this.renderingEngine = null;
|
||||||
|
cache.purgeVolumeCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the viewport inside the renderingEngine, if no viewport is left
|
||||||
|
* it destroys the renderingEngine.
|
||||||
|
* @param viewportIndex
|
||||||
|
*/
|
||||||
|
public disableElement(viewportIndex: number) {
|
||||||
|
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||||
|
if (!viewportInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportId = viewportInfo.getViewportId();
|
||||||
|
this.renderingEngine.disableElement(viewportId);
|
||||||
|
this.viewportsInfo.delete(viewportIndex);
|
||||||
|
|
||||||
|
if (this.viewportsInfo.size === 0) {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the renderingEngine to enable the element for the given viewport index
|
||||||
|
* and sets the displaySet data to the viewport
|
||||||
|
* @param {*} viewportIndex
|
||||||
|
* @param {*} displaySet
|
||||||
|
* @param {*} dataSource
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public setViewportDisplaySets(
|
||||||
|
viewportIndex: number,
|
||||||
|
viewportData: StackData | VolumeData,
|
||||||
|
publicViewportOptions: PublicViewportOptions,
|
||||||
|
publicDisplaySetOptions: DisplaySetOptions[]
|
||||||
|
): void {
|
||||||
|
const renderingEngine = this.getRenderingEngine();
|
||||||
|
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||||
|
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
||||||
|
|
||||||
|
const {
|
||||||
|
viewportOptions,
|
||||||
|
displaySetOptions,
|
||||||
|
} = this._getViewportAndDisplaySetOptions(
|
||||||
|
publicViewportOptions,
|
||||||
|
publicDisplaySetOptions,
|
||||||
|
viewportInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
viewportInfo.setViewportOptions(viewportOptions);
|
||||||
|
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
||||||
|
|
||||||
|
this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo);
|
||||||
|
|
||||||
|
const viewportId = viewportInfo.getViewportId();
|
||||||
|
const element = viewportInfo.getElement();
|
||||||
|
const type = viewportInfo.getViewportType();
|
||||||
|
const background = viewportInfo.getBackground();
|
||||||
|
const orientation = viewportInfo.getOrientation();
|
||||||
|
|
||||||
|
const viewportInput: Types.PublicViewportInput = {
|
||||||
|
viewportId,
|
||||||
|
element,
|
||||||
|
type,
|
||||||
|
defaultOptions: {
|
||||||
|
background,
|
||||||
|
orientation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Todo: this is not optimal at all, we are re-enabling the already enabled
|
||||||
|
// element which is not what we want. But enabledElement as part of the
|
||||||
|
// renderingEngine is designed to be used like this. This will trigger
|
||||||
|
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
|
||||||
|
renderingEngine.enableElement(viewportInput);
|
||||||
|
this._setDisplaySets(viewportId, viewportData, viewportInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCornerstoneViewport(
|
||||||
|
viewportId: string
|
||||||
|
): Types.IStackViewport | Types.IVolumeViewport | null {
|
||||||
|
const viewportInfo = this.getViewportInfo(viewportId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!viewportInfo ||
|
||||||
|
!this.renderingEngine ||
|
||||||
|
this.renderingEngine.hasBeenDestroyed
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = this.renderingEngine.getViewport(viewportId);
|
||||||
|
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCornerstoneViewportByIndex(
|
||||||
|
viewportIndex: number
|
||||||
|
): Types.IStackViewport | Types.IVolumeViewport | null {
|
||||||
|
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!viewportInfo ||
|
||||||
|
!this.renderingEngine ||
|
||||||
|
this.renderingEngine.hasBeenDestroyed
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewport = this.renderingEngine.getViewport(
|
||||||
|
viewportInfo.getViewportId()
|
||||||
|
);
|
||||||
|
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the viewportIndex for the provided viewportId
|
||||||
|
* @param {string} viewportId - the viewportId
|
||||||
|
* @returns {number} - the viewportIndex
|
||||||
|
*/
|
||||||
|
public getViewportInfoByIndex(viewportIndex: number): ViewportInfo {
|
||||||
|
return this.viewportsInfo.get(viewportIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportInfo(viewportId: string): ViewportInfo {
|
||||||
|
// @ts-ignore
|
||||||
|
for (const [index, viewport] of this.viewportsInfo.entries()) {
|
||||||
|
if (viewport.getViewportId() === viewportId) {
|
||||||
|
return viewport;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setStackViewport(
|
||||||
|
viewport: Types.IStackViewport,
|
||||||
|
viewportData: StackData,
|
||||||
|
viewportInfo: ViewportInfo
|
||||||
|
) {
|
||||||
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
|
const { imageIds, initialImageIndex } = viewportData;
|
||||||
|
|
||||||
|
let initialImageIndexToUse = initialImageIndex;
|
||||||
|
|
||||||
|
if (!initialImageIndexToUse) {
|
||||||
|
initialImageIndexToUse =
|
||||||
|
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { voi, voiInverted } = displaySetOptions[0];
|
||||||
|
const properties = {};
|
||||||
|
if (voi && (voi.windowWidth || voi.windowCenter)) {
|
||||||
|
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||||
|
voi.windowWidth,
|
||||||
|
voi.windowCenter
|
||||||
|
);
|
||||||
|
properties.voiRange = { lower, upper };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voiInverted !== undefined) {
|
||||||
|
properties.invert = voiInverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport.setStack(imageIds, initialImageIndexToUse).then(() => {
|
||||||
|
viewport.setProperties(properties);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getInitialImageIndexForStackViewport(
|
||||||
|
viewportInfo: ViewportInfo,
|
||||||
|
imageIds?: string[]
|
||||||
|
): number {
|
||||||
|
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||||
|
|
||||||
|
if (!initialImageOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { index, preset } = initialImageOptions;
|
||||||
|
return this._getInitialImageIndex(imageIds.length, index, preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInitialImageIndex(
|
||||||
|
numberOfSlices: number,
|
||||||
|
imageIndex?: number,
|
||||||
|
preset?: JumpPresets
|
||||||
|
): number {
|
||||||
|
const lastSliceIndex = numberOfSlices - 1;
|
||||||
|
|
||||||
|
if (imageIndex !== undefined) {
|
||||||
|
return csToolsUtils.clip(imageIndex, 0, lastSliceIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset === JumpPresets.First) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset === JumpPresets.Last) {
|
||||||
|
return lastSliceIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset === JumpPresets.Middle) {
|
||||||
|
return Math.floor(lastSliceIndex / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _setVolumeViewport(
|
||||||
|
viewport: Types.IVolumeViewport,
|
||||||
|
viewportData: VolumeData,
|
||||||
|
viewportInfo: ViewportInfo
|
||||||
|
): Promise<void> {
|
||||||
|
// TODO: We need to overhaul the way data sources work so requests can be made
|
||||||
|
// async. I think we should follow the image loader pattern which is async and
|
||||||
|
// has a cache behind it.
|
||||||
|
// The problem is that to set this volume, we need the metadata, but the request is
|
||||||
|
// already in-flight, and the promise is not cached, so we have no way to wait for
|
||||||
|
// it and know when it has fully arrived.
|
||||||
|
// loadStudyMetadata(StudyInstanceUID) => Promise([instances for study])
|
||||||
|
// loadSeriesMetadata(StudyInstanceUID, SeriesInstanceUID) => Promise([instances for series])
|
||||||
|
// If you call loadStudyMetadata and it's not in the DicomMetadataStore cache, it should fire
|
||||||
|
// a request through the data source?
|
||||||
|
// (This call may or may not create sub-requests for series metadata)
|
||||||
|
const volumeInputArray = [];
|
||||||
|
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
||||||
|
const { HangingProtocolService } = this;
|
||||||
|
|
||||||
|
for (let i = 0; i < viewportData.imageIds.length; i++) {
|
||||||
|
const imageIds = viewportData.imageIds[i];
|
||||||
|
const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
|
||||||
|
const displaySetOptions = displaySetOptionsArray[i];
|
||||||
|
|
||||||
|
const volumeId = displaySetInstanceUID;
|
||||||
|
|
||||||
|
// if (displaySet.needsRerendering) {
|
||||||
|
// console.warn('Removing volume from cache', volumeId);
|
||||||
|
// cache.removeVolumeLoadObject(volumeId);
|
||||||
|
// displaySet.needsRerendering = false;
|
||||||
|
// this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
|
||||||
|
// }
|
||||||
|
|
||||||
|
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
||||||
|
|
||||||
|
const callback = ({ volumeActor }) => {
|
||||||
|
voiCallbacks.forEach(callback => callback(volumeActor));
|
||||||
|
};
|
||||||
|
|
||||||
|
volumeInputArray.push({
|
||||||
|
imageIds,
|
||||||
|
volumeId,
|
||||||
|
callback,
|
||||||
|
blendMode: displaySetOptions.blendMode,
|
||||||
|
slabThickness: this._getSlabThickness(displaySetOptions, volumeId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
HangingProtocolService.hasCustomImageLoadStrategy() &&
|
||||||
|
!HangingProtocolService.customImageLoadPerformed
|
||||||
|
) {
|
||||||
|
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
||||||
|
return HangingProtocolService.runImageLoadStrategy({
|
||||||
|
viewportId: viewport.id,
|
||||||
|
volumeInputArray,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportData.volumes.forEach(volume => {
|
||||||
|
volume.load();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setVolumesForViewport(viewport, volumeInputArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setVolumesForViewport(viewport, volumeInputArray) {
|
||||||
|
viewport.setVolumes(volumeInputArray).then(() => {
|
||||||
|
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||||
|
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||||
|
|
||||||
|
if (
|
||||||
|
initialImageOptions &&
|
||||||
|
(initialImageOptions.preset !== undefined ||
|
||||||
|
initialImageOptions.index !== undefined)
|
||||||
|
) {
|
||||||
|
const { index, preset } = initialImageOptions;
|
||||||
|
|
||||||
|
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
|
||||||
|
viewport
|
||||||
|
);
|
||||||
|
|
||||||
|
const imageIndex = this._getInitialImageIndex(
|
||||||
|
numberOfSlices,
|
||||||
|
index,
|
||||||
|
preset
|
||||||
|
);
|
||||||
|
|
||||||
|
csToolsUtils.jumpToSlice(viewport.element, {
|
||||||
|
imageIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
viewport.render();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateViewport(viewportIndex, viewportData) {
|
||||||
|
const viewportInfo = this.getViewportInfoByIndex(viewportIndex);
|
||||||
|
|
||||||
|
const viewportId = viewportInfo.getViewportId();
|
||||||
|
const viewport = this.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
if (viewport instanceof VolumeViewport) {
|
||||||
|
this._setVolumeViewport(viewport, viewportData, viewportInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
this._setStackViewport(viewport, viewportData, viewportInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getVOICallbacks(volumeId, displaySetOptions) {
|
||||||
|
const { voi, voiInverted: inverted, colormap } = displaySetOptions;
|
||||||
|
|
||||||
|
const voiCallbackArray = [];
|
||||||
|
|
||||||
|
// If colormap is set, use it to set the color transfer function
|
||||||
|
if (colormap) {
|
||||||
|
voiCallbackArray.push(volumeActor => setColormap(volumeActor, colormap));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voi instanceof Object && voi.windowWidth && voi.windowCenter) {
|
||||||
|
const { windowWidth, windowCenter } = voi;
|
||||||
|
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||||
|
windowWidth,
|
||||||
|
windowCenter
|
||||||
|
);
|
||||||
|
voiCallbackArray.push(volumeActor =>
|
||||||
|
setLowerUpperColorTransferFunction({
|
||||||
|
volumeActor,
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
inverted,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return voiCallbackArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setDisplaySets(
|
||||||
|
viewportId: string,
|
||||||
|
viewportData: StackData | VolumeData,
|
||||||
|
viewportInfo: ViewportInfo
|
||||||
|
): void {
|
||||||
|
const viewport = this.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
if (viewport instanceof StackViewport) {
|
||||||
|
this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
|
||||||
|
} else if (viewport instanceof VolumeViewport) {
|
||||||
|
this._setVolumeViewport(
|
||||||
|
viewport,
|
||||||
|
viewportData as VolumeData,
|
||||||
|
viewportInfo
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unknown viewport type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the resize observer from the viewport element
|
||||||
|
*/
|
||||||
|
_removeResizeObserver() {
|
||||||
|
if (this.viewportGridResizeObserver) {
|
||||||
|
this.viewportGridResizeObserver.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSlabThickness(displaySetOptions, volumeId) {
|
||||||
|
const { blendMode } = displaySetOptions;
|
||||||
|
if (
|
||||||
|
blendMode === undefined ||
|
||||||
|
displaySetOptions.slabThickness === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a slabThickness set as a number then use it
|
||||||
|
if (typeof displaySetOptions.slabThickness === 'number') {
|
||||||
|
return displaySetOptions.slabThickness;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displaySetOptions.slabThickness.toLowerCase() === 'fullvolume') {
|
||||||
|
// calculate the slab thickness based on the volume dimensions
|
||||||
|
const imageVolume = cache.getVolume(volumeId);
|
||||||
|
|
||||||
|
const { dimensions } = imageVolume;
|
||||||
|
const slabThickness = Math.sqrt(
|
||||||
|
dimensions[0] * dimensions[0] +
|
||||||
|
dimensions[1] * dimensions[1] +
|
||||||
|
dimensions[2] * dimensions[2]
|
||||||
|
);
|
||||||
|
|
||||||
|
return slabThickness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getViewportAndDisplaySetOptions(
|
||||||
|
publicViewportOptions: PublicViewportOptions,
|
||||||
|
publicDisplaySetOptions: DisplaySetOptions[],
|
||||||
|
viewportInfo: ViewportInfo
|
||||||
|
): {
|
||||||
|
viewportOptions: ViewportOptions;
|
||||||
|
displaySetOptions: DisplaySetOptions[];
|
||||||
|
} {
|
||||||
|
const viewportIndex = viewportInfo.getViewportIndex();
|
||||||
|
|
||||||
|
// Creating a temporary viewportInfo to handle defaults
|
||||||
|
const newViewportInfo = new ViewportInfo(
|
||||||
|
viewportIndex,
|
||||||
|
viewportInfo.getViewportId()
|
||||||
|
);
|
||||||
|
|
||||||
|
// To handle setting the default values if missing for the viewportOptions and
|
||||||
|
// displaySetOptions
|
||||||
|
newViewportInfo.setPublicViewportOptions(publicViewportOptions);
|
||||||
|
newViewportInfo.setPublicDisplaySetOptions(publicDisplaySetOptions);
|
||||||
|
|
||||||
|
const newViewportOptions = newViewportInfo.getViewportOptions();
|
||||||
|
const newDisplaySetOptions = newViewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewportOptions: newViewportOptions,
|
||||||
|
displaySetOptions: newDisplaySetOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExtendedCornerstoneViewportService(serviceManager) {
|
||||||
|
return {
|
||||||
|
name: 'CornerstoneViewportService',
|
||||||
|
create: ({ configuration = {} }) => {
|
||||||
|
return new CornerstoneViewportService(serviceManager);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { Types } from '@cornerstonejs/core';
|
||||||
|
import { StackData, VolumeData } from './CornerstoneCacheService';
|
||||||
|
import {
|
||||||
|
DisplaySetOptions,
|
||||||
|
PublicViewportOptions,
|
||||||
|
ViewportOptions,
|
||||||
|
} from './Viewport';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles cornerstone viewport logic including enabling, disabling, and
|
||||||
|
* updating the viewport.
|
||||||
|
*/
|
||||||
|
export interface IViewportService {
|
||||||
|
servicesManager: unknown;
|
||||||
|
HangingProtocolService: unknown;
|
||||||
|
renderingEngine: unknown;
|
||||||
|
viewportGridResizeObserver: unknown;
|
||||||
|
viewportsInfo: unknown;
|
||||||
|
sceneVolumeInputs: unknown;
|
||||||
|
viewportIndexUIDMap: unknown;
|
||||||
|
viewportDivElements: unknown;
|
||||||
|
ViewportPropertiesMap: unknown;
|
||||||
|
volumeUIDs: unknown;
|
||||||
|
listeners: { [key: string]: {} };
|
||||||
|
displaySetsNeedRerendering: unknown;
|
||||||
|
viewportDisplaySets: unknown;
|
||||||
|
EVENTS: { [key: string]: string };
|
||||||
|
_broadcastEvent: unknown;
|
||||||
|
/**
|
||||||
|
* Adds the HTML element to the viewportService
|
||||||
|
* @param {*} viewportIndex
|
||||||
|
* @param {*} elementRef
|
||||||
|
*/
|
||||||
|
enableElement(
|
||||||
|
viewportIndex: number,
|
||||||
|
viewportOptions: ViewportOptions,
|
||||||
|
elementRef: HTMLDivElement
|
||||||
|
): void;
|
||||||
|
/**
|
||||||
|
* It retrieves the renderingEngine if it does exist, or creates one otherwise
|
||||||
|
* @returns {RenderingEngine} rendering engine
|
||||||
|
*/
|
||||||
|
getRenderingEngine(): Types.IRenderingEngine;
|
||||||
|
/**
|
||||||
|
* It creates a resize observer for the viewport element, and observes
|
||||||
|
* the element for resizing events
|
||||||
|
* @param {*} elementRef
|
||||||
|
*/
|
||||||
|
resize(element: HTMLDivElement): void;
|
||||||
|
/**
|
||||||
|
* Removes the viewport from cornerstone, and destroys the rendering engine
|
||||||
|
*/
|
||||||
|
destroy(): void;
|
||||||
|
/**
|
||||||
|
* Disables the viewport inside the renderingEngine, if no viewport is left
|
||||||
|
* it destroys the renderingEngine.
|
||||||
|
* @param viewportIndex
|
||||||
|
*/
|
||||||
|
disableElement(viewportIndex: number): void;
|
||||||
|
/**
|
||||||
|
* Uses the renderingEngine to enable the element for the given viewport index
|
||||||
|
* and sets the displaySet data to the viewport
|
||||||
|
* @param {*} viewportIndex
|
||||||
|
* @param {*} displaySet
|
||||||
|
* @param {*} dataSource
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
setViewportDisplaySets(
|
||||||
|
viewportIndex: number,
|
||||||
|
viewportData: StackData | VolumeData,
|
||||||
|
publicViewportOptions: PublicViewportOptions,
|
||||||
|
publicDisplaySetOptions: DisplaySetOptions[]
|
||||||
|
): void;
|
||||||
|
}
|
||||||
221
extensions/cornerstone/src/services/ViewportService/Viewport.ts
Normal file
221
extensions/cornerstone/src/services/ViewportService/Viewport.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { Types, Enums, CONSTANTS } from '@cornerstonejs/core';
|
||||||
|
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||||
|
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||||
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
|
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
||||||
|
|
||||||
|
export type InitialImageOptions = {
|
||||||
|
index?: number;
|
||||||
|
preset?: JumpPresets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ViewportOptions = {
|
||||||
|
viewportType: Enums.ViewportType;
|
||||||
|
toolGroupId: string;
|
||||||
|
viewportId: string;
|
||||||
|
orientation?: Types.Orientation;
|
||||||
|
background?: Types.Point3;
|
||||||
|
initialView?: string;
|
||||||
|
syncGroups?: SyncGroup[];
|
||||||
|
initialImageOptions?: InitialImageOptions;
|
||||||
|
customViewportOptions?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublicViewportOptions = {
|
||||||
|
viewportType?: string;
|
||||||
|
toolGroupId?: string;
|
||||||
|
viewportId?: string;
|
||||||
|
orientation?: string;
|
||||||
|
background?: Types.Point3;
|
||||||
|
initialView?: string;
|
||||||
|
syncGroups?: SyncGroup[];
|
||||||
|
initialImageOptions?: InitialImageOptions;
|
||||||
|
customViewportOptions?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PublicDisplaySetOptions = {
|
||||||
|
voi?: VOI;
|
||||||
|
voiInverted?: boolean;
|
||||||
|
blendMode?: string;
|
||||||
|
slabThickness?: number;
|
||||||
|
colormap?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DisplaySetOptions = {
|
||||||
|
voi?: VOI;
|
||||||
|
voiInverted: boolean;
|
||||||
|
blendMode?: Enums.BlendModes;
|
||||||
|
slabThickness?: number;
|
||||||
|
colormap?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VOI = {
|
||||||
|
windowWidth: number;
|
||||||
|
windowCenter: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DisplaySet = {
|
||||||
|
displaySetInstanceUID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STACK = 'stack';
|
||||||
|
const VOLUME = 'volume';
|
||||||
|
const DEFAULT_TOOLGROUP_ID = 'default';
|
||||||
|
|
||||||
|
class ViewportInfo {
|
||||||
|
private viewportId = '';
|
||||||
|
private viewportIndex: number;
|
||||||
|
private element: HTMLDivElement;
|
||||||
|
private viewportOptions: ViewportOptions;
|
||||||
|
private displaySetOptions: Array<DisplaySetOptions>;
|
||||||
|
private renderingEngineId: string;
|
||||||
|
|
||||||
|
constructor(viewportIndex: number, viewportId: string) {
|
||||||
|
this.viewportIndex = viewportIndex;
|
||||||
|
this.viewportId = viewportId;
|
||||||
|
this.setPublicViewportOptions({});
|
||||||
|
this.setPublicDisplaySetOptions([{}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRenderingEngineId(renderingEngineId: string): void {
|
||||||
|
this.renderingEngineId = renderingEngineId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRenderingEngineId(): string {
|
||||||
|
return this.renderingEngineId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewportId(viewportId: string): void {
|
||||||
|
this.viewportId = viewportId;
|
||||||
|
}
|
||||||
|
public setViewportIndex(viewportIndex: number): void {
|
||||||
|
this.viewportIndex = viewportIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setElement(element: HTMLDivElement): void {
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportIndex(): number {
|
||||||
|
return this.viewportIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getElement(): HTMLDivElement {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportId(): string {
|
||||||
|
return this.viewportId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPublicDisplaySetOptions(
|
||||||
|
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
||||||
|
): void {
|
||||||
|
// map the displaySetOptions and check if they are undefined then set them to default values
|
||||||
|
const displaySetOptions = this.mapDisplaySetOptions(
|
||||||
|
publicDisplaySetOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setDisplaySetOptions(displaySetOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPublicViewportOptions(
|
||||||
|
viewportOptionsEntry: PublicViewportOptions
|
||||||
|
): void {
|
||||||
|
let viewportType = viewportOptionsEntry.viewportType;
|
||||||
|
let toolGroupId = viewportOptionsEntry.toolGroupId;
|
||||||
|
let orientation;
|
||||||
|
|
||||||
|
if (!viewportType) {
|
||||||
|
viewportType = getCornerstoneViewportType(STACK);
|
||||||
|
} else {
|
||||||
|
viewportType = getCornerstoneViewportType(
|
||||||
|
viewportOptionsEntry.viewportType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// map SAGITTAL, AXIAL, CORONAL orientation to be used by cornerstone
|
||||||
|
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
|
||||||
|
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||||
|
} else {
|
||||||
|
orientation = CONSTANTS.ORIENTATION.AXIAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toolGroupId) {
|
||||||
|
toolGroupId = DEFAULT_TOOLGROUP_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setViewportOptions({
|
||||||
|
...viewportOptionsEntry,
|
||||||
|
viewportId: this.viewportId,
|
||||||
|
viewportType: viewportType as Enums.ViewportType,
|
||||||
|
orientation,
|
||||||
|
toolGroupId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewportOptions(viewportOptions: ViewportOptions): void {
|
||||||
|
this.viewportOptions = viewportOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportOptions(): ViewportOptions {
|
||||||
|
return this.viewportOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDisplaySetOptions(
|
||||||
|
displaySetOptions: Array<DisplaySetOptions>
|
||||||
|
): void {
|
||||||
|
this.displaySetOptions = displaySetOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSyncGroups(): SyncGroup[] {
|
||||||
|
return this.viewportOptions.syncGroups || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDisplaySetOptions(): Array<DisplaySetOptions> {
|
||||||
|
return this.displaySetOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportType(): Enums.ViewportType {
|
||||||
|
return this.viewportOptions.viewportType || Enums.ViewportType.STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getToolGroupId(): string {
|
||||||
|
return this.viewportOptions.toolGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getBackground(): Types.Point3 {
|
||||||
|
return this.viewportOptions.background || [0, 0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getOrientation(): Types.Orientation {
|
||||||
|
return this.viewportOptions.orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInitialImageOptions(): InitialImageOptions {
|
||||||
|
return this.viewportOptions.initialImageOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapDisplaySetOptions(
|
||||||
|
publicDisplaySetOptions: Array<PublicDisplaySetOptions>
|
||||||
|
): Array<DisplaySetOptions> {
|
||||||
|
const displaySetOptions: Array<DisplaySetOptions> = [];
|
||||||
|
|
||||||
|
publicDisplaySetOptions.forEach(option => {
|
||||||
|
const blendMode = getCornerstoneBlendMode(option.blendMode);
|
||||||
|
|
||||||
|
displaySetOptions.push({
|
||||||
|
voi: option.voi || ({} as VOI),
|
||||||
|
voiInverted: option.voiInverted || false,
|
||||||
|
colormap: option.colormap || undefined,
|
||||||
|
slabThickness: option.slabThickness,
|
||||||
|
blendMode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return displaySetOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ViewportInfo;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const RENDERING_ENGINE_ID = 'OHIFCornerstoneRenderingEngine';
|
||||||
|
|
||||||
|
export { RENDERING_ENGINE_ID };
|
||||||
@ -1,6 +1,6 @@
|
|||||||
const state = {
|
const state = {
|
||||||
// The `defaultContext` of an extension's commandsModule
|
// The `defaultContext` of an extension's commandsModule
|
||||||
DEFAULT_CONTEXT: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
DEFAULT_CONTEXT: 'CORNERSTONE',
|
||||||
enabledElements: {},
|
enabledElements: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -9,7 +9,11 @@ const state = {
|
|||||||
* @param {HTMLElement} dom Active viewport element.
|
* @param {HTMLElement} dom Active viewport element.
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
const setEnabledElement = (viewportIndex, element, context) => {
|
const setEnabledElement = (
|
||||||
|
viewportIndex: number,
|
||||||
|
element: HTMLElement,
|
||||||
|
context?: string
|
||||||
|
): void => {
|
||||||
const targetContext = context || state.DEFAULT_CONTEXT;
|
const targetContext = context || state.DEFAULT_CONTEXT;
|
||||||
|
|
||||||
state.enabledElements[viewportIndex] = {
|
state.enabledElements[viewportIndex] = {
|
||||||
@ -19,7 +23,7 @@ const setEnabledElement = (viewportIndex, element, context) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grabs the enabled element `dom` reference of an adective viewport.
|
* Grabs the enabled element `dom` reference of an ative viewport.
|
||||||
*
|
*
|
||||||
* @return {HTMLElement} Active viewport element.
|
* @return {HTMLElement} Active viewport element.
|
||||||
*/
|
*/
|
||||||
@ -1,123 +0,0 @@
|
|||||||
// Visible?
|
|
||||||
// Disabled?
|
|
||||||
// Based on contexts or misc. criteria?
|
|
||||||
// -- ACTIVE_ROUTE::VIEWER
|
|
||||||
// -- ACTIVE_VIEWPORT::CORNERSTONE
|
|
||||||
// setToolActive commands should receive the button event that triggered
|
|
||||||
// so we can do the "bind to this button" magic
|
|
||||||
|
|
||||||
// const definitions = [
|
|
||||||
// // OLD
|
|
||||||
// {
|
|
||||||
// id: 'StackScroll',
|
|
||||||
// label: 'Stack Scroll',
|
|
||||||
// icon: 'bars',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
// commandName: 'setToolActive',
|
|
||||||
// commandOptions: { toolName: 'StackScroll' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'Reset',
|
|
||||||
// label: 'Reset',
|
|
||||||
// icon: 'reset',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'resetViewport',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'Cine',
|
|
||||||
// label: 'CINE',
|
|
||||||
// icon: 'youtube',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
|
||||||
// options: {
|
|
||||||
// behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'More',
|
|
||||||
// label: 'More',
|
|
||||||
// icon: 'ellipse-circle',
|
|
||||||
// buttons: [
|
|
||||||
// {
|
|
||||||
// id: 'Magnify',
|
|
||||||
// label: 'Magnify',
|
|
||||||
// icon: 'circle',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
// commandName: 'setToolActive',
|
|
||||||
// commandOptions: { toolName: 'Magnify' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'WwwcRegion',
|
|
||||||
// label: 'ROI Window',
|
|
||||||
// icon: 'stop',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
// commandName: 'setToolActive',
|
|
||||||
// commandOptions: { toolName: 'WwwcRegion' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'Invert',
|
|
||||||
// label: 'Invert',
|
|
||||||
// icon: 'adjust',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'invertViewport',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'RotateRight',
|
|
||||||
// label: 'Rotate Right',
|
|
||||||
// icon: 'rotate-right',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'rotateViewportCW',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'FlipH',
|
|
||||||
// label: 'Flip H',
|
|
||||||
// icon: 'ellipse-h',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'flipViewportHorizontal',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'FlipV',
|
|
||||||
// label: 'Flip V',
|
|
||||||
// icon: 'ellipse-v',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'flipViewportVertical',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'Clear',
|
|
||||||
// label: 'Clear',
|
|
||||||
// icon: 'trash',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'clearAnnotations',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'Eraser',
|
|
||||||
// label: 'Eraser',
|
|
||||||
// icon: 'eraser',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
|
||||||
// commandName: 'setToolActive',
|
|
||||||
// commandOptions: { toolName: 'Eraser' },
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'Exit2DMPR',
|
|
||||||
// label: 'Exit 2D MPR',
|
|
||||||
// icon: 'times',
|
|
||||||
// //
|
|
||||||
// type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
|
||||||
// commandName: 'setCornerstoneLayout',
|
|
||||||
// context: 'ACTIVE_VIEWPORT::VTK',
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
export default [];
|
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import domtoimage from 'dom-to-image';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Enums,
|
||||||
|
getEnabledElement,
|
||||||
|
getOrCreateCanvas,
|
||||||
|
StackViewport,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ViewportDownloadForm } from '@ohif/ui';
|
||||||
|
|
||||||
|
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||||
|
|
||||||
|
const MINIMUM_SIZE = 100;
|
||||||
|
const DEFAULT_SIZE = 512;
|
||||||
|
const MAX_TEXTURE_SIZE = 10000;
|
||||||
|
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
|
||||||
|
const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup';
|
||||||
|
|
||||||
|
const CornerstoneViewportDownloadForm = ({
|
||||||
|
onClose,
|
||||||
|
activeViewportIndex,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
}) => {
|
||||||
|
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
|
||||||
|
const activeViewportElement = enabledElement?.element;
|
||||||
|
|
||||||
|
const enableViewport = viewportElement => {
|
||||||
|
if (viewportElement) {
|
||||||
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||||
|
|
||||||
|
const viewportInput = {
|
||||||
|
viewportId: VIEWPORT_ID,
|
||||||
|
element: viewportElement,
|
||||||
|
type: Enums.ViewportType.STACK,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderingEngine.enableElement(viewportInput);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disableViewport = viewportElement => {
|
||||||
|
if (viewportElement) {
|
||||||
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
renderingEngine.disableElement(VIEWPORT_ID);
|
||||||
|
ToolGroupManager.destroyToolGroup(TOOLGROUP_ID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateViewportPreview = (
|
||||||
|
downloadViewportElement,
|
||||||
|
internalCanvas,
|
||||||
|
fileType
|
||||||
|
) =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
const enabledElement = getEnabledElement(downloadViewportElement);
|
||||||
|
|
||||||
|
const { viewport: downloadViewport, renderingEngine } = enabledElement;
|
||||||
|
|
||||||
|
// Note: Since any trigger of dimensions will update the viewport,
|
||||||
|
// we need to resize the offScreenCanvas to accommodate for the new
|
||||||
|
// dimensions, this is due to the reason that we are using the GPU offScreenCanvas
|
||||||
|
// to render the viewport for the downloadViewport.
|
||||||
|
renderingEngine.resize();
|
||||||
|
|
||||||
|
// Trigger the render on the viewport to update the on screen
|
||||||
|
downloadViewport.render();
|
||||||
|
|
||||||
|
downloadViewportElement.addEventListener(
|
||||||
|
Enums.Events.IMAGE_RENDERED,
|
||||||
|
function updateViewport(event) {
|
||||||
|
const enabledElement = getEnabledElement(event.target);
|
||||||
|
const { viewport } = enabledElement;
|
||||||
|
const { element } = viewport;
|
||||||
|
|
||||||
|
const downloadCanvas = getOrCreateCanvas(element);
|
||||||
|
|
||||||
|
const type = 'image/' + fileType;
|
||||||
|
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
||||||
|
|
||||||
|
let newWidth = element.offsetHeight;
|
||||||
|
let newHeight = element.offsetWidth;
|
||||||
|
|
||||||
|
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
||||||
|
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
||||||
|
newHeight *= multiplier;
|
||||||
|
newWidth *= multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||||
|
|
||||||
|
downloadViewportElement.removeEventListener(
|
||||||
|
Enums.Events.IMAGE_RENDERED,
|
||||||
|
updateViewport
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadImage = (activeViewportElement, viewportElement, width, height) =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
if (activeViewportElement && viewportElement) {
|
||||||
|
const activeViewportEnabledElement = getEnabledElement(
|
||||||
|
activeViewportElement
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!activeViewportEnabledElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewport } = activeViewportEnabledElement;
|
||||||
|
|
||||||
|
if (!(viewport instanceof StackViewport)) {
|
||||||
|
throw new Error('Viewport is not a StackViewport');
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageId = viewport.getCurrentImageId();
|
||||||
|
|
||||||
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||||
|
const downloadViewport = renderingEngine.getViewport(
|
||||||
|
VIEWPORT_ID
|
||||||
|
) as StackViewport;
|
||||||
|
|
||||||
|
downloadViewport.setStack([imageId]).then(() => {
|
||||||
|
const properties = viewport.getProperties();
|
||||||
|
downloadViewport.setProperties(properties);
|
||||||
|
|
||||||
|
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||||
|
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||||
|
|
||||||
|
resolve({ width: newWidth, height: newHeight });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAnnotations = (
|
||||||
|
toggle,
|
||||||
|
viewportElement,
|
||||||
|
activeViewportElement
|
||||||
|
) => {
|
||||||
|
const activeViewportEnabledElement = getEnabledElement(
|
||||||
|
activeViewportElement
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadViewportElement = getEnabledElement(viewportElement);
|
||||||
|
|
||||||
|
const {
|
||||||
|
viewportId: activeViewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
} = activeViewportEnabledElement;
|
||||||
|
const { viewportId: downloadViewportId } = downloadViewportElement;
|
||||||
|
|
||||||
|
if (!activeViewportEnabledElement || !downloadViewportElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
|
activeViewportId,
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
|
||||||
|
let downloadToolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
|
downloadViewportId,
|
||||||
|
renderingEngineId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (downloadToolGroup === undefined) {
|
||||||
|
downloadToolGroup = ToolGroupManager.createToolGroup(TOOLGROUP_ID);
|
||||||
|
|
||||||
|
// what tools were in the active viewport?
|
||||||
|
// make them all enabled instances so that they can not be interacted
|
||||||
|
// with in the download viewport
|
||||||
|
Object.values(toolGroup._toolInstances).forEach(tool => {
|
||||||
|
downloadToolGroup.addTool(tool.getToolName());
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the viewport to the toolGroup
|
||||||
|
downloadToolGroup.addViewport(downloadViewportId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(downloadToolGroup._toolInstances).forEach(tool => {
|
||||||
|
const toolName = tool.getToolName();
|
||||||
|
if (toggle) {
|
||||||
|
downloadToolGroup.setToolEnabled(toolName);
|
||||||
|
} else {
|
||||||
|
downloadToolGroup.setToolDisabled(toolName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadBlob = (filename, fileType) => {
|
||||||
|
const file = `${filename}.${fileType}`;
|
||||||
|
const divForDownloadViewport = document.querySelector(
|
||||||
|
`div[data-viewport-uid="${VIEWPORT_ID}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
domtoimage.toPng(divForDownloadViewport).then(dataUrl => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = file;
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ViewportDownloadForm
|
||||||
|
onClose={onClose}
|
||||||
|
minimumSize={MINIMUM_SIZE}
|
||||||
|
maximumSize={MAX_TEXTURE_SIZE}
|
||||||
|
defaultSize={DEFAULT_SIZE}
|
||||||
|
canvasClass={'cornerstone-canvas'}
|
||||||
|
activeViewportElement={activeViewportElement}
|
||||||
|
enableViewport={enableViewport}
|
||||||
|
disableViewport={disableViewport}
|
||||||
|
updateViewportPreview={updateViewportPreview}
|
||||||
|
loadImage={loadImage}
|
||||||
|
toggleAnnotations={toggleAnnotations}
|
||||||
|
downloadBlob={downloadBlob}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CornerstoneViewportDownloadForm.propTypes = {
|
||||||
|
onClose: PropTypes.func,
|
||||||
|
activeViewportIndex: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CornerstoneViewportDownloadForm;
|
||||||
14
extensions/cornerstone/src/utils/JumpPresets.ts
Normal file
14
extensions/cornerstone/src/utils/JumpPresets.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Jump Presets - This enum defines the 3 jump states which are available
|
||||||
|
* to be used with the jumpToSlice utility function.
|
||||||
|
*/
|
||||||
|
enum JumpPresets {
|
||||||
|
/** Jumps to first slice */
|
||||||
|
First = 'first',
|
||||||
|
/** Jumps to last slice */
|
||||||
|
Last = 'last',
|
||||||
|
/** Jumps to the middle slice */
|
||||||
|
Middle = 'middle',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default JumpPresets;
|
||||||
139
extensions/cornerstone/src/utils/colormap/applyPreset.js
Normal file
139
extensions/cornerstone/src/utils/colormap/applyPreset.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
|
||||||
|
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
|
||||||
|
|
||||||
|
import presets from './presets';
|
||||||
|
|
||||||
|
// get preset from preset name
|
||||||
|
function getPreset(name) {
|
||||||
|
return presets.find(preset => preset.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: we should be able to register a new preset via the utilityModule
|
||||||
|
// of the cornerstone extension
|
||||||
|
export default function applyPreset(actor, presetName) {
|
||||||
|
const preset = getPreset(presetName);
|
||||||
|
// Create color transfer function
|
||||||
|
const colorTransferArray = preset.colorTransfer
|
||||||
|
.split(' ')
|
||||||
|
.splice(1)
|
||||||
|
.map(parseFloat);
|
||||||
|
|
||||||
|
const { shiftRange } = getShiftRange(colorTransferArray);
|
||||||
|
let min = shiftRange[0];
|
||||||
|
const width = shiftRange[1] - shiftRange[0];
|
||||||
|
const cfun = vtkColorTransferFunction.newInstance();
|
||||||
|
const normColorTransferValuePoints = [];
|
||||||
|
for (let i = 0; i < colorTransferArray.length; i += 4) {
|
||||||
|
let value = colorTransferArray[i];
|
||||||
|
const r = colorTransferArray[i + 1];
|
||||||
|
const g = colorTransferArray[i + 2];
|
||||||
|
const b = colorTransferArray[i + 3];
|
||||||
|
|
||||||
|
value = (value - min) / width;
|
||||||
|
normColorTransferValuePoints.push([value, r, g, b]);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPointsToRGBFunction(normColorTransferValuePoints, shiftRange, cfun);
|
||||||
|
|
||||||
|
actor.getProperty().setRGBTransferFunction(0, cfun);
|
||||||
|
|
||||||
|
// Create scalar opacity function
|
||||||
|
const scalarOpacityArray = preset.scalarOpacity
|
||||||
|
.split(' ')
|
||||||
|
.splice(1)
|
||||||
|
.map(parseFloat);
|
||||||
|
|
||||||
|
const ofun = vtkPiecewiseFunction.newInstance();
|
||||||
|
const normPoints = [];
|
||||||
|
for (let i = 0; i < scalarOpacityArray.length; i += 2) {
|
||||||
|
let value = scalarOpacityArray[i];
|
||||||
|
const opacity = scalarOpacityArray[i + 1];
|
||||||
|
|
||||||
|
value = (value - min) / width;
|
||||||
|
|
||||||
|
normPoints.push([value, opacity]);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPointsToPiecewiseFunction(normPoints, shiftRange, ofun);
|
||||||
|
|
||||||
|
actor.getProperty().setScalarOpacity(0, ofun);
|
||||||
|
|
||||||
|
const [
|
||||||
|
gradientMinValue,
|
||||||
|
gradientMinOpacity,
|
||||||
|
gradientMaxValue,
|
||||||
|
gradientMaxOpacity,
|
||||||
|
] = preset.gradientOpacity
|
||||||
|
.split(' ')
|
||||||
|
.splice(1)
|
||||||
|
.map(parseFloat);
|
||||||
|
|
||||||
|
actor.getProperty().setUseGradientOpacity(0, true);
|
||||||
|
actor.getProperty().setGradientOpacityMinimumValue(0, gradientMinValue);
|
||||||
|
actor.getProperty().setGradientOpacityMinimumOpacity(0, gradientMinOpacity);
|
||||||
|
actor.getProperty().setGradientOpacityMaximumValue(0, gradientMaxValue);
|
||||||
|
actor.getProperty().setGradientOpacityMaximumOpacity(0, gradientMaxOpacity);
|
||||||
|
|
||||||
|
if (preset.interpolation === '1') {
|
||||||
|
actor.getProperty().setInterpolationTypeToFastLinear();
|
||||||
|
//actor.getProperty().setInterpolationTypeToLinear()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ambient = parseFloat(preset.ambient);
|
||||||
|
//const shade = preset.shade === '1'
|
||||||
|
const diffuse = parseFloat(preset.diffuse);
|
||||||
|
const specular = parseFloat(preset.specular);
|
||||||
|
const specularPower = parseFloat(preset.specularPower);
|
||||||
|
|
||||||
|
//actor.getProperty().setShade(shade)
|
||||||
|
actor.getProperty().setAmbient(ambient);
|
||||||
|
actor.getProperty().setDiffuse(diffuse);
|
||||||
|
actor.getProperty().setSpecular(specular);
|
||||||
|
actor.getProperty().setSpecularPower(specularPower);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShiftRange(colorTransferArray) {
|
||||||
|
// Credit to paraview-glance
|
||||||
|
// https://github.com/Kitware/paraview-glance/blob/3fec8eeff31e9c19ad5b6bff8e7159bd745e2ba9/src/components/controls/ColorBy/script.js#L133
|
||||||
|
|
||||||
|
// shift range is original rgb/opacity range centered around 0
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
for (let i = 0; i < colorTransferArray.length; i += 4) {
|
||||||
|
min = Math.min(min, colorTransferArray[i]);
|
||||||
|
max = Math.max(max, colorTransferArray[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const center = (max - min) / 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
shiftRange: [-center, center],
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPointsToRGBFunction(points, range, cfun) {
|
||||||
|
const width = range[1] - range[0];
|
||||||
|
const rescaled = points.map(([x, r, g, b]) => [
|
||||||
|
x * width + range[0],
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
]);
|
||||||
|
|
||||||
|
cfun.removeAllPoints();
|
||||||
|
rescaled.forEach(([x, r, g, b]) => cfun.addRGBPoint(x, r, g, b));
|
||||||
|
|
||||||
|
return rescaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPointsToPiecewiseFunction(points, range, pwf) {
|
||||||
|
const width = range[1] - range[0];
|
||||||
|
const rescaled = points.map(([x, y]) => [x * width + range[0], y]);
|
||||||
|
|
||||||
|
pwf.removeAllPoints();
|
||||||
|
rescaled.forEach(([x, y]) => pwf.addPoint(x, y));
|
||||||
|
|
||||||
|
return rescaled;
|
||||||
|
}
|
||||||
29
extensions/cornerstone/src/utils/colormap/colors.js
Normal file
29
extensions/cornerstone/src/utils/colormap/colors.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// https://www.slicer.org/w/index.php/Slicer3:2010_GenericAnatomyColors#Lookup_table
|
||||||
|
const colors = [
|
||||||
|
{
|
||||||
|
integerLabel: 0,
|
||||||
|
textLabel: 'background',
|
||||||
|
color: [0, 0, 0, 0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
integerLabel: 1,
|
||||||
|
textLabel: 'tissue',
|
||||||
|
// color: [255, 174, 128, 255],
|
||||||
|
color: [255, 0, 0, 255],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
integerLabel: 2,
|
||||||
|
textLabel: 'bone',
|
||||||
|
// color: [241, 214, 145, 255],
|
||||||
|
color: [0, 255, 0, 255],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
integerLabel: 3,
|
||||||
|
textLabel: 'skin',
|
||||||
|
// color: [177, 122, 101, 255],
|
||||||
|
color: [0, 0, 255, 255],
|
||||||
|
},
|
||||||
|
// ....
|
||||||
|
];
|
||||||
|
|
||||||
|
export default colors;
|
||||||
435
extensions/cornerstone/src/utils/colormap/presets.js
Normal file
435
extensions/cornerstone/src/utils/colormap/presets.js
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
const presets = [
|
||||||
|
{
|
||||||
|
name: 'CT-AAA',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'12 -3024 0 143.556 0 166.222 0.686275 214.389 0.696078 419.736 0.833333 3071 0.803922',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode1',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'24 -3024 0 0 0 143.556 0.615686 0.356863 0.184314 166.222 0.882353 0.603922 0.290196 214.389 1 1 1 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '143.556 419.736',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-AAA2',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'16 -3024 0 129.542 0 145.244 0.166667 157.02 0.5 169.918 0.627451 395.575 0.8125 1578.73 0.8125 3071 0.8125',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode2',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'32 -3024 0 0 0 129.542 0.54902 0.25098 0.14902 145.244 0.6 0.627451 0.843137 157.02 0.890196 0.47451 0.6 169.918 0.992157 0.870588 0.392157 395.575 1 0.886275 0.658824 1578.73 1 0.829256 0.957922 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '0 1600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Bone',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity: '8 -3024 0 -16.4458 0 641.385 0.715686 3071 0.705882',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode3',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'16 -3024 0 0 0 -16.4458 0.729412 0.254902 0.301961 641.385 0.905882 0.815686 0.552941 3071 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-16.4458 641.385',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Bones',
|
||||||
|
gradientOpacity: '4 0 1 985.12 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '8 -1000 0 152.19 0 278.93 0.190476 952 0.2',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode4',
|
||||||
|
specular: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'20 -1000 0.3 0.3 1 -488 0.3 1 0.3 463.28 1 0 0 659.15 1 0.912535 0.0374849 953 1 0.3 0.3',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '152.19 952',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Cardiac',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'12 -3024 0 -77.6875 0 94.9518 0.285714 179.052 0.553571 260.439 0.848214 3071 0.875',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode5',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'24 -3024 0 0 0 -77.6875 0.54902 0.25098 0.14902 94.9518 0.882353 0.603922 0.290196 179.052 1 0.937033 0.954531 260.439 0.615686 0 0 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-77.6875 260.439',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Cardiac2',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'12 -3024 0 42.8964 0 163.488 0.428571 277.642 0.776786 1587 0.754902 3071 0.754902',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode6',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'24 -3024 0 0 0 42.8964 0.54902 0.25098 0.14902 163.488 0.917647 0.639216 0.0588235 277.642 1 0.878431 0.623529 1587 1 1 1 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '42.8964 1587',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Cardiac3',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'14 -3024 0 -86.9767 0 45.3791 0.169643 139.919 0.589286 347.907 0.607143 1224.16 0.607143 3071 0.616071',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode7',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'28 -3024 0 0 0 -86.9767 0 0.25098 1 45.3791 1 0 0 139.919 1 0.894893 0.894893 347.907 1 1 0.25098 1224.16 1 1 1 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-86.9767 1224.16',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Chest-Contrast-Enhanced',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'10 -3024 0 67.0106 0 251.105 0.446429 439.291 0.625 3071 0.616071',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode8',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'20 -3024 0 0 0 67.0106 0.54902 0.25098 0.14902 251.105 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '67.0106 439.291',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Chest-Vessels',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity:
|
||||||
|
'10 -3024 0 -1278.35 0 22.8277 0.428571 439.291 0.625 3071 0.616071',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode9',
|
||||||
|
specular: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'20 -3024 0 0 0 -1278.35 0.54902 0.25098 0.14902 22.8277 0.882353 0.603922 0.290196 439.291 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-1278.35 439.291',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Coronary-Arteries',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity:
|
||||||
|
'12 -2048 0 136.47 0 159.215 0.258929 318.43 0.571429 478.693 0.776786 3661 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode10',
|
||||||
|
specular: '0',
|
||||||
|
shade: '0',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'24 -2048 0 0 0 136.47 0 0 0 159.215 0.159804 0.159804 0.159804 318.43 0.764706 0.764706 0.764706 478.693 1 1 1 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '136.47 478.693',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Coronary-Arteries-2',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity:
|
||||||
|
'14 -2048 0 142.677 0 145.016 0.116071 192.174 0.5625 217.24 0.776786 384.347 0.830357 3661 0.830357',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode11',
|
||||||
|
specular: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'28 -2048 0 0 0 142.677 0 0 0 145.016 0.615686 0 0.0156863 192.174 0.909804 0.454902 0 217.24 0.972549 0.807843 0.611765 384.347 0.909804 0.909804 1 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '142.677 384.347',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Coronary-Arteries-3',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'14 -2048 0 128.643 0 129.982 0.0982143 173.636 0.669643 255.884 0.857143 584.878 0.866071 3661 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode12',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'28 -2048 0 0 0 128.643 0 0 0 129.982 0.615686 0 0.0156863 173.636 0.909804 0.454902 0 255.884 0.886275 0.886275 0.886275 584.878 0.968627 0.968627 0.968627 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '128.643 584.878',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Cropped-Volume-Bone',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '10 -2048 0 -451 0 -450 1 1050 1 3661 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode13',
|
||||||
|
specular: '0',
|
||||||
|
shade: '0',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'20 -2048 0 0 0 -451 0 0 0 -450 0.0556356 0.0556356 0.0556356 1050 1 1 1 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-451 1050',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Fat',
|
||||||
|
gradientOpacity: '6 0 1 985.12 1 988 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '14 -1000 0 -100 0 -99 0.15 -60 0.15 -59 0 101.2 0 952 0',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode14',
|
||||||
|
specular: '0',
|
||||||
|
references: '0',
|
||||||
|
shade: '0',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'36 -1000 0.3 0.3 1 -497.5 0.3 1 0.3 -99 0 0 1 -76.946 0 1 0 -65.481 0.835431 0.888889 0.0165387 83.89 1 0 0 463.28 1 0 0 659.15 1 0.912535 0.0374849 2952 1 0.300267 0.299886',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-100 101.2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Liver-Vasculature',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity:
|
||||||
|
'14 -2048 0 149.113 0 157.884 0.482143 339.96 0.660714 388.526 0.830357 1197.95 0.839286 3661 0.848214',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode15',
|
||||||
|
specular: '0',
|
||||||
|
shade: '0',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'28 -2048 0 0 0 149.113 0 0 0 157.884 0.501961 0.25098 0 339.96 0.695386 0.59603 0.36886 388.526 0.854902 0.85098 0.827451 1197.95 1 1 1 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '149.113 1197.95',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Lung',
|
||||||
|
gradientOpacity: '6 0 1 985.12 1 988 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '12 -1000 0 -600 0 -599 0.15 -400 0.15 -399 0 2952 0',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode16',
|
||||||
|
specular: '0',
|
||||||
|
references: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'24 -1000 0.3 0.3 1 -600 0 0 1 -530 0.134704 0.781726 0.0724558 -460 0.929244 1 0.109473 -400 0.888889 0.254949 0.0240258 2952 1 0.3 0.3',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-600 -399',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-MIP',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity: '8 -3024 0 -637.62 0 700 1 3071 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode17',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer: '16 -3024 0 0 0 -637.62 1 1 1 700 1 1 1 3071 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-637.62 700',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Muscle',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity:
|
||||||
|
'10 -3024 0 -155.407 0 217.641 0.676471 419.736 0.833333 3071 0.803922',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode18',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'20 -3024 0 0 0 -155.407 0.54902 0.25098 0.14902 217.641 0.882353 0.603922 0.290196 419.736 1 0.937033 0.954531 3071 0.827451 0.658824 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-155.407 419.736',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Pulmonary-Arteries',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity:
|
||||||
|
'14 -2048 0 -568.625 0 -364.081 0.0714286 -244.813 0.401786 18.2775 0.607143 447.798 0.830357 3592.73 0.839286',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode19',
|
||||||
|
specular: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'28 -2048 0 0 0 -568.625 0 0 0 -364.081 0.396078 0.301961 0.180392 -244.813 0.611765 0.352941 0.0705882 18.2775 0.843137 0.0156863 0.156863 447.798 0.752941 0.752941 0.752941 3592.73 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-568.625 447.798',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Soft-Tissue',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '10 -2048 0 -167.01 0 -160 1 240 1 3661 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode20',
|
||||||
|
specular: '0',
|
||||||
|
shade: '0',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'20 -2048 0 0 0 -167.01 0 0 0 -160 0.0556356 0.0556356 0.0556356 240 1 1 1 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-167.01 240',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'CT-Air',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '10',
|
||||||
|
scalarOpacity: '8 -3024 0.705882 -900.0 0.715686 -500.0 0 3071 0',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode21',
|
||||||
|
specular: '0.2',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.1',
|
||||||
|
colorTransfer:
|
||||||
|
'16 -3024 1 1 1 -900.0 0.2 1.0 1.0 -500.0 0.3 0.3 1.0 3071 0 0 0 ',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '-1200.0 -200.0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MR-Angio',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity:
|
||||||
|
'12 -2048 0 151.354 0 158.279 0.4375 190.112 0.580357 200.873 0.732143 3661 0.741071',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode22',
|
||||||
|
specular: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'24 -2048 0 0 0 151.354 0 0 0 158.279 0.74902 0.376471 0 190.112 1 0.866667 0.733333 200.873 0.937255 0.937255 0.937255 3661 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '151.354 200.873',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MR-Default',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '12 0 0 20 0 40 0.15 120 0.3 220 0.375 1024 0.5',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode23',
|
||||||
|
specular: '0',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer:
|
||||||
|
'24 0 0 0 0 20 0.168627 0 0 40 0.403922 0.145098 0.0784314 120 0.780392 0.607843 0.380392 220 0.847059 0.835294 0.788235 1024 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '0 220',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MR-MIP',
|
||||||
|
gradientOpacity: '4 0 1 255 1',
|
||||||
|
specularPower: '1',
|
||||||
|
scalarOpacity: '8 0 0 98.3725 0 416.637 1 2800 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode24',
|
||||||
|
specular: '0',
|
||||||
|
shade: '0',
|
||||||
|
ambient: '0.2',
|
||||||
|
colorTransfer: '16 0 1 1 1 98.3725 1 1 1 416.637 1 1 1 2800 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '1',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '0 416.637',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MR-T2-Brain',
|
||||||
|
gradientOpacity: '4 0 1 160.25 1',
|
||||||
|
specularPower: '40',
|
||||||
|
scalarOpacity: '10 0 0 36.05 0 218.302 0.171429 412.406 1 641 1',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode25',
|
||||||
|
specular: '0.5',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.3',
|
||||||
|
colorTransfer:
|
||||||
|
'16 0 0 0 0 98.7223 0.956863 0.839216 0.192157 412.406 0 0.592157 0.807843 641 1 1 1',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.6',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '0 412.406',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DTI-FA-Brain',
|
||||||
|
gradientOpacity: '4 0 1 0.9950 1',
|
||||||
|
specularPower: '40',
|
||||||
|
scalarOpacity:
|
||||||
|
'16 0 0 0 0 0.3501 0.0158 0.49379 0.7619 0.6419 1 0.9920 1 0.9950 0 0.9950 0',
|
||||||
|
id: 'vtkMRMLVolumePropertyNode26',
|
||||||
|
specular: '0.5',
|
||||||
|
shade: '1',
|
||||||
|
ambient: '0.3',
|
||||||
|
colorTransfer:
|
||||||
|
'28 0 1 0 0 0 1 0 0 0.24974 0.4941 1 0 0.49949 0 0.9882 1 0.7492 0.51764 0 1 0.9950 1 0 0 0.9950 1 0 0',
|
||||||
|
selectable: 'true',
|
||||||
|
diffuse: '0.9',
|
||||||
|
interpolation: '1',
|
||||||
|
effectiveRange: '0 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default presets;
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import { cache, utilities } from '@cornerstonejs/core';
|
||||||
|
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
||||||
|
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
|
||||||
|
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
|
||||||
|
|
||||||
|
const colormaps = {};
|
||||||
|
|
||||||
|
export function registerColormap(colormap) {
|
||||||
|
colormaps[colormap.Name] = colormap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColorTransferFunctionFromVolumeMetadata({
|
||||||
|
volumeActor,
|
||||||
|
volumeId,
|
||||||
|
inverted,
|
||||||
|
}) {
|
||||||
|
let lower, upper, windowWidth, windowCenter;
|
||||||
|
|
||||||
|
if (volumeId) {
|
||||||
|
const volume = cache.getVolume(volumeId);
|
||||||
|
const voiLutModule = volume.metadata.voiLut[0];
|
||||||
|
if (voiLutModule) {
|
||||||
|
windowWidth = voiLutModule.windowWidth;
|
||||||
|
windowCenter = voiLutModule.windowCenter;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
windowWidth = 400;
|
||||||
|
windowCenter = 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowWidth == undefined || windowCenter === undefined) {
|
||||||
|
// Set to something so we can window level it manually.
|
||||||
|
lower = 200;
|
||||||
|
upper = 400;
|
||||||
|
} else {
|
||||||
|
lower = windowCenter - windowWidth / 2.0;
|
||||||
|
upper = windowCenter + windowWidth / 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLowerUpperColorTransferFunction({ volumeActor, lower, upper, inverted });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLowerUpperColorTransferFunction({
|
||||||
|
volumeActor,
|
||||||
|
lower,
|
||||||
|
upper,
|
||||||
|
inverted,
|
||||||
|
}) {
|
||||||
|
volumeActor
|
||||||
|
.getProperty()
|
||||||
|
.getRGBTransferFunction(0)
|
||||||
|
.setMappingRange(lower, upper);
|
||||||
|
|
||||||
|
if (inverted) {
|
||||||
|
utilities.invertRgbTransferFunction(
|
||||||
|
volumeActor.getProperty().getRGBTransferFunction(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColormap(volumeActor, colormap) {
|
||||||
|
const mapper = volumeActor.getMapper();
|
||||||
|
mapper.setSampleDistance(1.0);
|
||||||
|
|
||||||
|
const cfun = vtkColorTransferFunction.newInstance();
|
||||||
|
|
||||||
|
// if we have a custom colormap, use it
|
||||||
|
let preset;
|
||||||
|
if (colormaps[colormap]) {
|
||||||
|
preset = colormaps[colormap];
|
||||||
|
} else {
|
||||||
|
preset = vtkColorMaps.getPresetByName(colormap);
|
||||||
|
}
|
||||||
|
|
||||||
|
cfun.applyColorMap(preset);
|
||||||
|
cfun.setMappingRange(0, 5);
|
||||||
|
|
||||||
|
volumeActor.getProperty().setRGBTransferFunction(0, cfun);
|
||||||
|
|
||||||
|
// Create scalar opacity function
|
||||||
|
const ofun = vtkPiecewiseFunction.newInstance();
|
||||||
|
ofun.addPoint(0, 0.0);
|
||||||
|
ofun.addPoint(0.1, 0.9);
|
||||||
|
ofun.addPoint(5, 1.0);
|
||||||
|
|
||||||
|
volumeActor.getProperty().setScalarOpacity(0, ofun);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
setColormap,
|
||||||
|
setColorTransferFunctionFromVolumeMetadata,
|
||||||
|
setLowerUpperColorTransferFunction,
|
||||||
|
};
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import { imageLoader } from '@cornerstonejs/core';
|
||||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../DICOMWeb';
|
import { DICOMWeb, errorHandler } from '@ohif/core';
|
||||||
|
|
||||||
import errorHandler from '../errorHandler';
|
|
||||||
|
|
||||||
const getImageId = imageObj => {
|
const getImageId = imageObj => {
|
||||||
if (!imageObj) {
|
if (!imageObj) {
|
||||||
@ -49,7 +47,7 @@ const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cornerstoneRetriever = imageId => {
|
const cornerstoneRetriever = imageId => {
|
||||||
return cornerstone.loadAndCacheImage(imageId).then(image => {
|
return imageLoader.loadAndCacheImage(imageId).then(image => {
|
||||||
return image && image.data && image.data.byteArray.buffer;
|
return image && image.data && image.data.byteArray.buffer;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -151,6 +149,10 @@ class DicomLoaderService {
|
|||||||
}
|
}
|
||||||
getDicomDataMethod = fetchIt.bind(this, imageId);
|
getDicomDataMethod = fetchIt.bind(this, imageId);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported image type: ${loaderType} for imageId: ${imageId}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDicomDataMethod();
|
return getDicomDataMethod();
|
||||||
17
extensions/cornerstone/src/utils/getCornerstoneBlendMode.ts
Normal file
17
extensions/cornerstone/src/utils/getCornerstoneBlendMode.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const MIP = 'mip';
|
||||||
|
|
||||||
|
export default function getCornerstoneBlendMode(
|
||||||
|
blendMode: string
|
||||||
|
): Enums.BlendModes {
|
||||||
|
if (!blendMode) {
|
||||||
|
return Enums.BlendModes.COMPOSITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blendMode.toLowerCase() === MIP) {
|
||||||
|
return Enums.BlendModes.MAXIMUM_INTENSITY_BLEND;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import cornerstoneTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
|
|
||||||
|
|
||||||
export default function getCornerstoneMeasurementById(id) {
|
|
||||||
const globalToolState = globalImageIdSpecificToolStateManager.saveToolState();
|
|
||||||
|
|
||||||
const imageIds = Object.keys(globalToolState);
|
|
||||||
|
|
||||||
for (let i = 0; i < imageIds.length; i++) {
|
|
||||||
const imageId = imageIds[i];
|
|
||||||
const imageIdSpecificToolState = globalToolState[imageId];
|
|
||||||
|
|
||||||
const toolTypes = Object.keys(imageIdSpecificToolState);
|
|
||||||
|
|
||||||
for (let j = 0; j < toolTypes.length; j++) {
|
|
||||||
const toolType = toolTypes[j];
|
|
||||||
const toolData = imageIdSpecificToolState[toolType].data;
|
|
||||||
|
|
||||||
if (toolData) {
|
|
||||||
for (let k = 0; k < toolData.length; k++) {
|
|
||||||
const toolDataK = toolData[k];
|
|
||||||
|
|
||||||
if (toolDataK.id === id) {
|
|
||||||
return toolDataK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { CONSTANTS } from '@cornerstonejs/core';
|
||||||
|
import { log } from '@ohif/core';
|
||||||
|
|
||||||
|
const AXIAL = 'axial';
|
||||||
|
const SAGITTAL = 'sagittal';
|
||||||
|
const CORONAL = 'coronal';
|
||||||
|
|
||||||
|
export default function getCornerstoneOrientation(
|
||||||
|
orientation: string
|
||||||
|
): CONSTANTS.ORIENTATION {
|
||||||
|
switch (orientation.toLowerCase()) {
|
||||||
|
case AXIAL:
|
||||||
|
return CONSTANTS.ORIENTATION.AXIAL;
|
||||||
|
case SAGITTAL:
|
||||||
|
return CONSTANTS.ORIENTATION.SAGITTAL;
|
||||||
|
case CORONAL:
|
||||||
|
return CONSTANTS.ORIENTATION.CORONAL;
|
||||||
|
default:
|
||||||
|
log.wanr('Choosing default orientation: axial');
|
||||||
|
return CONSTANTS.ORIENTATION.AXIAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const STACK = 'stack';
|
||||||
|
const VOLUME = 'volume';
|
||||||
|
|
||||||
|
export default function getCornerstoneViewportType(
|
||||||
|
viewportType: string
|
||||||
|
): Enums.ViewportType {
|
||||||
|
if (viewportType.toLowerCase() === STACK) {
|
||||||
|
return Enums.ViewportType.STACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewportType.toLowerCase() === VOLUME) {
|
||||||
|
return Enums.ViewportType.ORTHOGRAPHIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Invalid viewport type: ${viewportType}. Valid types are: stack, volume`
|
||||||
|
);
|
||||||
|
}
|
||||||
60
extensions/cornerstone/src/utils/getInterleavedFrames.js
Normal file
60
extensions/cornerstone/src/utils/getInterleavedFrames.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
export default function getInterleavedFrames(imageIds) {
|
||||||
|
const minImageIdIndex = 0;
|
||||||
|
const maxImageIdIndex = imageIds.length - 1;
|
||||||
|
|
||||||
|
const middleImageIdIndex = Math.floor(imageIds.length / 2);
|
||||||
|
|
||||||
|
let lowerImageIdIndex = middleImageIdIndex;
|
||||||
|
let upperImageIdIndex = middleImageIdIndex;
|
||||||
|
|
||||||
|
// Build up an array of images to prefetch, starting with the current image.
|
||||||
|
const imageIdsToPrefetch = [
|
||||||
|
{ imageId: imageIds[middleImageIdIndex], imageIdIndex: middleImageIdIndex },
|
||||||
|
];
|
||||||
|
|
||||||
|
const prefetchQueuedFilled = {
|
||||||
|
currentPositionDownToMinimum: false,
|
||||||
|
currentPositionUpToMaximum: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if on edges and some criteria is already fulfilled
|
||||||
|
|
||||||
|
if (middleImageIdIndex === minImageIdIndex) {
|
||||||
|
prefetchQueuedFilled.currentPositionDownToMinimum = true;
|
||||||
|
} else if (middleImageIdIndex === maxImageIdIndex) {
|
||||||
|
prefetchQueuedFilled.currentPositionUpToMaximum = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (
|
||||||
|
!prefetchQueuedFilled.currentPositionDownToMinimum ||
|
||||||
|
!prefetchQueuedFilled.currentPositionUpToMaximum
|
||||||
|
) {
|
||||||
|
if (!prefetchQueuedFilled.currentPositionDownToMinimum) {
|
||||||
|
// Add imageId bellow
|
||||||
|
lowerImageIdIndex--;
|
||||||
|
imageIdsToPrefetch.push({
|
||||||
|
imageId: imageIds[lowerImageIdIndex],
|
||||||
|
imageIdIndex: lowerImageIdIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lowerImageIdIndex === minImageIdIndex) {
|
||||||
|
prefetchQueuedFilled.currentPositionDownToMinimum = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prefetchQueuedFilled.currentPositionUpToMaximum) {
|
||||||
|
// Add imageId above
|
||||||
|
upperImageIdIndex++;
|
||||||
|
imageIdsToPrefetch.push({
|
||||||
|
imageId: imageIds[upperImageIdIndex],
|
||||||
|
imageIdIndex: upperImageIdIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (upperImageIdIndex === maxImageIdIndex) {
|
||||||
|
prefetchQueuedFilled.currentPositionUpToMaximum = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageIdsToPrefetch;
|
||||||
|
}
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import csTools from 'cornerstone-tools';
|
|
||||||
|
|
||||||
const toolsGroupedByType = {
|
|
||||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
|
||||||
annotations: [
|
|
||||||
csTools.ArrowAnnotateTool,
|
|
||||||
csTools.BidirectionalTool,
|
|
||||||
csTools.LengthTool,
|
|
||||||
csTools.AngleTool,
|
|
||||||
csTools.FreehandRoiTool,
|
|
||||||
csTools.EllipticalRoiTool,
|
|
||||||
csTools.DragProbeTool,
|
|
||||||
csTools.RectangleRoiTool,
|
|
||||||
],
|
|
||||||
other: [
|
|
||||||
csTools.PanTool,
|
|
||||||
csTools.ZoomTool,
|
|
||||||
csTools.WwwcTool,
|
|
||||||
csTools.WwwcRegionTool,
|
|
||||||
csTools.MagnifyTool,
|
|
||||||
csTools.StackScrollTool,
|
|
||||||
csTools.StackScrollMouseWheelTool,
|
|
||||||
csTools.OverlayTool,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function getTools() {
|
|
||||||
const tools = [];
|
|
||||||
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
|
|
||||||
tools.push(...toolsGroupedByType[toolsGroup])
|
|
||||||
);
|
|
||||||
|
|
||||||
return tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { toolsGroupedByType };
|
|
||||||
150
extensions/cornerstone/src/utils/interleaveCenterLoader.ts
Normal file
150
extensions/cornerstone/src/utils/interleaveCenterLoader.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
|
||||||
|
import getInterleavedFrames from './getInterleavedFrames';
|
||||||
|
import { compact, flatten, zip } from 'lodash';
|
||||||
|
|
||||||
|
// Map of volumeId and SeriesInstanceId
|
||||||
|
const volumeIdMapsToLoad = new Map<string, string>();
|
||||||
|
const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function caches the volumeUIDs until all the volumes inside the
|
||||||
|
* hangging protocol are initialized. Then it goes through the imageIds
|
||||||
|
* of the volumes, and interleav them, in order for the volumes to be loaded
|
||||||
|
* together from middle to the start and the end.
|
||||||
|
* @param {Object} props image loading properties from Cornerstone ViewportService
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function interleaveCenterLoader({
|
||||||
|
data: { viewportId, volumeInputArray },
|
||||||
|
displaySetsMatchDetails,
|
||||||
|
matchDetails,
|
||||||
|
}) {
|
||||||
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
|
// Based on the volumeInputs store the volumeIds and SeriesInstanceIds
|
||||||
|
// to keep track of the volumes being loaded
|
||||||
|
for (const volumeInput of volumeInputArray) {
|
||||||
|
const { volumeId } = volumeInput;
|
||||||
|
const volume = cache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (!volume) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the volumeUID is not in the volumeUIDs array, add it
|
||||||
|
if (!volumeIdMapsToLoad.has(volumeId)) {
|
||||||
|
const { metadata } = volume;
|
||||||
|
volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following is checking if all the viewports that were matched in the HP has been
|
||||||
|
* successfully created their cornerstone viewport or not. Todo: This can be
|
||||||
|
* improved by not checking it, and as soon as the matched DisplaySets have their
|
||||||
|
* volume loaded, we start the loading, but that comes at the cost of viewports
|
||||||
|
* not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one
|
||||||
|
* pt viewport are created we have a guarantee that the volumes are created in the cache
|
||||||
|
* but the rest of the viewports (fusion, mip etc.) are not created yet. So
|
||||||
|
* we can't initiate setting the volumes for those viewports. One solution can be
|
||||||
|
* to add an event when a viewport is created (not enabled element event) and then
|
||||||
|
* listen to it and as the other viewports are created we can set the volumes for them
|
||||||
|
* since volumes are already started loading.
|
||||||
|
*/
|
||||||
|
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all the matched volumes are loaded
|
||||||
|
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||||
|
const { SeriesInstanceUID } = details;
|
||||||
|
|
||||||
|
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||||
|
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||||
|
// get volumes from cache
|
||||||
|
const volumes = volumeIds.map(volumeId => {
|
||||||
|
return cache.getVolume(volumeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// iterate over all volumes, and get their imageIds, and interleave
|
||||||
|
// the imageIds and save them in AllRequests for later use
|
||||||
|
const AllRequests = [];
|
||||||
|
volumes.forEach(volume => {
|
||||||
|
const requests = volume.getImageLoadRequests();
|
||||||
|
|
||||||
|
if (!requests.length || !requests[0] || !requests[0].imageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestImageIds = requests.map(request => {
|
||||||
|
return request.imageId;
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageIds = getInterleavedFrames(requestImageIds);
|
||||||
|
|
||||||
|
const reOrderedRequests = imageIds.map(({ imageId }) => {
|
||||||
|
const request = requests.find(req => req.imageId === imageId);
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
|
||||||
|
AllRequests.push(reOrderedRequests);
|
||||||
|
});
|
||||||
|
|
||||||
|
// flatten the AllRequests array, which will result in a list of all the
|
||||||
|
// imageIds for all the volumes but interleaved
|
||||||
|
const interleavedRequests = compact(flatten(zip(...AllRequests)));
|
||||||
|
|
||||||
|
// set the finalRequests to the imageLoadPoolManager
|
||||||
|
const finalRequests = [];
|
||||||
|
interleavedRequests.forEach(request => {
|
||||||
|
const { imageId } = request;
|
||||||
|
|
||||||
|
AllRequests.forEach(volumeRequests => {
|
||||||
|
const volumeImageIdRequest = volumeRequests.find(
|
||||||
|
req => req.imageId === imageId
|
||||||
|
);
|
||||||
|
if (volumeImageIdRequest) {
|
||||||
|
finalRequests.push(volumeImageIdRequest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestType = Enums.RequestType.Prefetch;
|
||||||
|
const priority = 0;
|
||||||
|
|
||||||
|
finalRequests.forEach(
|
||||||
|
({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => {
|
||||||
|
const callLoadImageBound = callLoadImage.bind(
|
||||||
|
null,
|
||||||
|
imageId,
|
||||||
|
imageIdIndex,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
imageLoadPoolManager.addRequest(
|
||||||
|
callLoadImageBound,
|
||||||
|
requestType,
|
||||||
|
additionalDetails,
|
||||||
|
priority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// clear the volumeIdMapsToLoad
|
||||||
|
volumeIdMapsToLoad.clear();
|
||||||
|
|
||||||
|
// copy the viewportIdVolumeInputArrayMap
|
||||||
|
const viewportIdVolumeInputArrayMapCopy = new Map(
|
||||||
|
viewportIdVolumeInputArrayMap
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset the viewportIdVolumeInputArrayMap
|
||||||
|
viewportIdVolumeInputArrayMap.clear();
|
||||||
|
|
||||||
|
return viewportIdVolumeInputArrayMapCopy;
|
||||||
|
}
|
||||||
139
extensions/cornerstone/src/utils/interleaveTopToBottom.ts
Normal file
139
extensions/cornerstone/src/utils/interleaveTopToBottom.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
|
||||||
|
import { compact, flatten, zip } from 'lodash';
|
||||||
|
|
||||||
|
// Map of volumeId and SeriesInstanceId
|
||||||
|
const volumeIdMapsToLoad = new Map<string, string>();
|
||||||
|
const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function caches the volumeIds until all the volumes inside the
|
||||||
|
* hanging protocol are initialized. Then it goes through the imageIds
|
||||||
|
* of the volumes, and interleave them, in order for the volumes to be loaded
|
||||||
|
* together from middle to the start and the end.
|
||||||
|
* @param {Object} {viewportData, displaySetMatchDetails}
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function interleaveTopToBottom({
|
||||||
|
data: { viewportId, volumeInputArray },
|
||||||
|
displaySetsMatchDetails,
|
||||||
|
matchDetails,
|
||||||
|
}) {
|
||||||
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
|
// Based on the volumeInputs store the volumeIds and SeriesInstanceIds
|
||||||
|
// to keep track of the volumes being loaded
|
||||||
|
for (const volumeInput of volumeInputArray) {
|
||||||
|
const { volumeId } = volumeInput;
|
||||||
|
const volume = cache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (!volume) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the volumeUID is not in the volumeUIDs array, add it
|
||||||
|
if (!volumeIdMapsToLoad.has(volumeId)) {
|
||||||
|
const { metadata } = volume;
|
||||||
|
volumeIdMapsToLoad.set(volumeId, metadata.SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following is checking if all the viewports that were matched in the HP has been
|
||||||
|
* successfully created their cornerstone viewport or not. Todo: This can be
|
||||||
|
* improved by not checking it, and as soon as the matched DisplaySets have their
|
||||||
|
* volume loaded, we start the loading, but that comes at the cost of viewports
|
||||||
|
* not being created yet (e.g., in a 10 viewport ptCT fusion, when one ct viewport and one
|
||||||
|
* pt viewport are created we have a guarantee that the volumes are created in the cache
|
||||||
|
* but the rest of the viewports (fusion, mip etc.) are not created yet. So
|
||||||
|
* we can't initiate setting the volumes for those viewports. One solution can be
|
||||||
|
* to add an event when a viewport is created (not enabled element event) and then
|
||||||
|
* listen to it and as the other viewports are created we can set the volumes for them
|
||||||
|
* since volumes are already started loading.
|
||||||
|
*/
|
||||||
|
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if all the matched volumes are loaded
|
||||||
|
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||||
|
const { SeriesInstanceUID } = details;
|
||||||
|
|
||||||
|
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||||
|
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeIds = Array.from(volumeIdMapsToLoad.keys()).slice();
|
||||||
|
// get volumes from cache
|
||||||
|
const volumes = volumeIds.map(volumeId => {
|
||||||
|
return cache.getVolume(volumeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// iterate over all volumes, and get their imageIds, and interleave
|
||||||
|
// the imageIds and save them in AllRequests for later use
|
||||||
|
const AllRequests = [];
|
||||||
|
volumes.forEach(volume => {
|
||||||
|
const requests = volume.getImageLoadRequests();
|
||||||
|
|
||||||
|
if (!requests.length || !requests[0] || !requests[0].imageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reverse the requests
|
||||||
|
AllRequests.push(requests.reverse());
|
||||||
|
});
|
||||||
|
|
||||||
|
// flatten the AllRequests array, which will result in a list of all the
|
||||||
|
// imageIds for all the volumes but interleaved
|
||||||
|
const interleavedRequests = compact(flatten(zip(...AllRequests)));
|
||||||
|
|
||||||
|
// set the finalRequests to the imageLoadPoolManager
|
||||||
|
const finalRequests = [];
|
||||||
|
interleavedRequests.forEach(request => {
|
||||||
|
const { imageId } = request;
|
||||||
|
|
||||||
|
AllRequests.forEach(volumeRequests => {
|
||||||
|
const volumeImageIdRequest = volumeRequests.find(
|
||||||
|
req => req.imageId === imageId
|
||||||
|
);
|
||||||
|
if (volumeImageIdRequest) {
|
||||||
|
finalRequests.push(volumeImageIdRequest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestType = Enums.RequestType.Prefetch;
|
||||||
|
const priority = 0;
|
||||||
|
|
||||||
|
finalRequests.forEach(
|
||||||
|
({ callLoadImage, additionalDetails, imageId, imageIdIndex, options }) => {
|
||||||
|
const callLoadImageBound = callLoadImage.bind(
|
||||||
|
null,
|
||||||
|
imageId,
|
||||||
|
imageIdIndex,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
imageLoadPoolManager.addRequest(
|
||||||
|
callLoadImageBound,
|
||||||
|
requestType,
|
||||||
|
additionalDetails,
|
||||||
|
priority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// clear the volumeIdMapsToLoad
|
||||||
|
volumeIdMapsToLoad.clear();
|
||||||
|
|
||||||
|
// copy the viewportIdVolumeInputArrayMap
|
||||||
|
const viewportIdVolumeInputArrayMapCopy = new Map(
|
||||||
|
viewportIdVolumeInputArrayMap
|
||||||
|
);
|
||||||
|
|
||||||
|
// reset the viewportIdVolumeInputArrayMap
|
||||||
|
viewportIdVolumeInputArrayMap.clear();
|
||||||
|
|
||||||
|
return viewportIdVolumeInputArrayMapCopy;
|
||||||
|
}
|
||||||
@ -1,57 +1,142 @@
|
|||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getPointsFromHandles from './utils/getPointsFromHandles';
|
|
||||||
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
const ArrowAnnotate = {
|
const Length = {
|
||||||
toAnnotation: (measurement, definition) => {},
|
toAnnotation: measurement => {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps cornerstone annotation event data to measurement service format.
|
||||||
|
*
|
||||||
|
* @param {Object} cornerstone Cornerstone event data
|
||||||
|
* @return {Measurement} Measurement instance
|
||||||
|
*/
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsAnnotation,
|
csToolsEventDetail,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
getValueTypeFromToolType
|
getValueTypeFromToolType
|
||||||
) => {
|
) => {
|
||||||
const { element, measurementData } = csToolsAnnotation;
|
const { annotation, viewportId } = csToolsEventDetail;
|
||||||
const tool =
|
const { metadata, data, annotationUID } = annotation;
|
||||||
csToolsAnnotation.toolType ||
|
|
||||||
csToolsAnnotation.toolName ||
|
|
||||||
measurementData.toolType;
|
|
||||||
|
|
||||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
if (!metadata || !data) {
|
||||||
|
console.warn('Length tool: Missing metadata or data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validToolType(tool)) {
|
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||||
|
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType) {
|
||||||
throw new Error('Tool not supported');
|
throw new Error('Tool not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
} = getSOPInstanceAttributes(element);
|
} = getSOPInstanceAttributes(
|
||||||
|
referencedImageId,
|
||||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
CornerstoneViewportService,
|
||||||
SOPInstanceUID,
|
viewportId
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const points = [];
|
let displaySet;
|
||||||
points.push(measurementData.handles);
|
|
||||||
|
if (SOPInstanceUID) {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { points } = data.handles;
|
||||||
|
|
||||||
|
const mappedAnnotations = getMappedAnnotations(
|
||||||
|
annotation,
|
||||||
|
DisplaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
uid: annotationUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
|
points,
|
||||||
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: measurementData.text,
|
label: data.text,
|
||||||
description: measurementData.description,
|
text: data.text,
|
||||||
unit: measurementData.unit,
|
displayText: displayText,
|
||||||
text: measurementData.text,
|
data: data.cachedStats,
|
||||||
type: getValueTypeFromToolType(tool),
|
type: getValueTypeFromToolType(toolName),
|
||||||
points: getPointsFromHandles(measurementData.handles),
|
getReport: () => {
|
||||||
|
throw new Error('Not implemented');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArrowAnnotate;
|
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||||
|
const { metadata, data } = annotation;
|
||||||
|
const { text } = data;
|
||||||
|
const { referencedImageId } = metadata;
|
||||||
|
|
||||||
|
const annotations = [];
|
||||||
|
|
||||||
|
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
||||||
|
referencedImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SeriesNumber } = displaySet;
|
||||||
|
|
||||||
|
annotations.push({
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesNumber,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayText(mappedAnnotations, displaySet) {
|
||||||
|
if (!mappedAnnotations) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayText = [];
|
||||||
|
|
||||||
|
// Area is the same for all series
|
||||||
|
const { SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
||||||
|
|
||||||
|
const instance = displaySet.images.find(
|
||||||
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let InstanceNumber;
|
||||||
|
if (instance) {
|
||||||
|
InstanceNumber = instance.InstanceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayText.push(
|
||||||
|
InstanceNumber
|
||||||
|
? `(S: ${SeriesNumber} I: ${InstanceNumber})`
|
||||||
|
: `(S: ${SeriesNumber})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return displayText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Length;
|
||||||
|
|||||||
@ -1,59 +1,197 @@
|
|||||||
|
import { annotation } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
const Bidirectional = {
|
const Bidirectional = {
|
||||||
toAnnotation: (measurement, definition) => {},
|
toAnnotation: measurement => {},
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsAnnotation,
|
csToolsEventDetail,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
getValueTypeFromToolType
|
getValueTypeFromToolType
|
||||||
) => {
|
) => {
|
||||||
const { element, measurementData } = csToolsAnnotation;
|
const { annotation, viewportId } = csToolsEventDetail;
|
||||||
const tool =
|
const { metadata, data, annotationUID } = annotation;
|
||||||
csToolsAnnotation.toolType ||
|
|
||||||
csToolsAnnotation.toolName ||
|
|
||||||
measurementData.toolType;
|
|
||||||
|
|
||||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
if (!metadata || !data) {
|
||||||
|
console.warn('Length tool: Missing metadata or data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validToolType(tool)) {
|
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||||
|
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType) {
|
||||||
throw new Error('Tool not supported');
|
throw new Error('Tool not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
} = getSOPInstanceAttributes(element);
|
} = getSOPInstanceAttributes(
|
||||||
|
referencedImageId,
|
||||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
CornerstoneViewportService,
|
||||||
SOPInstanceUID,
|
viewportId
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { handles } = measurementData;
|
let displaySet;
|
||||||
|
|
||||||
const longAxis = [handles.start, handles.end];
|
if (SOPInstanceUID) {
|
||||||
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { points } = data.handles;
|
||||||
|
|
||||||
|
const mappedAnnotations = getMappedAnnotations(
|
||||||
|
annotation,
|
||||||
|
DisplaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||||
|
const getReport = () =>
|
||||||
|
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
uid: annotationUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
|
points,
|
||||||
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: measurementData.label,
|
label: data.label,
|
||||||
description: measurementData.description,
|
displayText: displayText,
|
||||||
unit: measurementData.unit,
|
data: data.cachedStats,
|
||||||
shortestDiameter: measurementData.shortestDiameter,
|
type: getValueTypeFromToolType(toolName),
|
||||||
longestDiameter: measurementData.longestDiameter,
|
getReport,
|
||||||
type: getValueTypeFromToolType(tool),
|
|
||||||
points: { longAxis, shortAxis },
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||||
|
const { metadata, data } = annotation;
|
||||||
|
const { cachedStats } = data;
|
||||||
|
const { referencedImageId, referencedSeriesInstanceUID } = metadata;
|
||||||
|
const targets = Object.keys(cachedStats);
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotations = [];
|
||||||
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (referencedImageId) {
|
||||||
|
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
||||||
|
referencedImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
||||||
|
const { length, width } = targetStats;
|
||||||
|
const unit = 'mm';
|
||||||
|
|
||||||
|
annotations.push({
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SeriesNumber,
|
||||||
|
unit,
|
||||||
|
length,
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This function is used to convert the measurement data to a format that is
|
||||||
|
suitable for the report generation (e.g. for the csv report). The report
|
||||||
|
returns a list of columns and corresponding values.
|
||||||
|
*/
|
||||||
|
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||||
|
const columns = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
// Add Type
|
||||||
|
columns.push('AnnotationType');
|
||||||
|
values.push('Cornerstone:Bidirectional');
|
||||||
|
|
||||||
|
mappedAnnotations.forEach(annotation => {
|
||||||
|
const { length, width } = annotation;
|
||||||
|
columns.push(`Length (mm)`, `Width (mm)`);
|
||||||
|
values.push(length, width);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (FrameOfReferenceUID) {
|
||||||
|
columns.push('FrameOfReferenceUID');
|
||||||
|
values.push(FrameOfReferenceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points) {
|
||||||
|
columns.push('points');
|
||||||
|
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||||
|
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||||
|
// so that it can be used in the csv report
|
||||||
|
values.push(points.map(p => p.join(' ')).join(';'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayText(mappedAnnotations, displaySet) {
|
||||||
|
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayText = [];
|
||||||
|
|
||||||
|
// Area is the same for all series
|
||||||
|
const { length, width, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
||||||
|
const roundedLength = utils.roundNumber(length, 1);
|
||||||
|
const roundedWidth = utils.roundNumber(width, 1);
|
||||||
|
|
||||||
|
const instance = displaySet.images.find(
|
||||||
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let InstanceNumber;
|
||||||
|
if (instance) {
|
||||||
|
InstanceNumber = instance.InstanceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayText.push(
|
||||||
|
InstanceNumber
|
||||||
|
? `L: ${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
|
||||||
|
: `L: ${roundedLength} mm (S: ${SeriesNumber})`
|
||||||
|
);
|
||||||
|
displayText.push(`W: ${roundedWidth} mm`);
|
||||||
|
|
||||||
|
return displayText;
|
||||||
|
}
|
||||||
|
|
||||||
export default Bidirectional;
|
export default Bidirectional;
|
||||||
|
|||||||
@ -0,0 +1,219 @@
|
|||||||
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
import getModalityUnit from './utils/getModalityUnit';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
const EllipticalROI = {
|
||||||
|
toAnnotation: measurement => {},
|
||||||
|
toMeasurement: (
|
||||||
|
csToolsEventDetail,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
getValueTypeFromToolType
|
||||||
|
) => {
|
||||||
|
const { annotation, viewportId } = csToolsEventDetail;
|
||||||
|
const { metadata, data, annotationUID } = annotation;
|
||||||
|
|
||||||
|
if (!metadata || !data) {
|
||||||
|
console.warn('Length tool: Missing metadata or data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||||
|
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType) {
|
||||||
|
throw new Error('Tool not supported');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = getSOPInstanceAttributes(
|
||||||
|
referencedImageId,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
viewportId
|
||||||
|
);
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (SOPInstanceUID) {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { points } = data.handles;
|
||||||
|
|
||||||
|
const mappedAnnotations = getMappedAnnotations(
|
||||||
|
annotation,
|
||||||
|
DisplaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||||
|
const getReport = () =>
|
||||||
|
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uid: annotationUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
points,
|
||||||
|
metadata,
|
||||||
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
toolName: metadata.toolName,
|
||||||
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
|
label: data.label,
|
||||||
|
displayText: displayText,
|
||||||
|
data: data.cachedStats,
|
||||||
|
type: getValueTypeFromToolType(toolName),
|
||||||
|
getReport,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||||
|
const { metadata, data } = annotation;
|
||||||
|
const { cachedStats } = data;
|
||||||
|
const { referencedImageId } = metadata;
|
||||||
|
const targets = Object.keys(cachedStats);
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotations = [];
|
||||||
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (referencedImageId) {
|
||||||
|
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
||||||
|
referencedImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Todo: Non-acquisition plane measurement mapping not supported yet
|
||||||
|
throw new Error(
|
||||||
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
||||||
|
const { mean, stdDev, max, area, Modality } = targetStats;
|
||||||
|
const unit = getModalityUnit(Modality);
|
||||||
|
|
||||||
|
annotations.push({
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SeriesNumber,
|
||||||
|
Modality,
|
||||||
|
unit,
|
||||||
|
mean,
|
||||||
|
stdDev,
|
||||||
|
max,
|
||||||
|
area,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This function is used to convert the measurement data to a format that is
|
||||||
|
suitable for the report generation (e.g. for the csv report). The report
|
||||||
|
returns a list of columns and corresponding values.
|
||||||
|
*/
|
||||||
|
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||||
|
const columns = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
// Add Type
|
||||||
|
columns.push('AnnotationType');
|
||||||
|
values.push('Cornerstone:EllipticalROI');
|
||||||
|
|
||||||
|
mappedAnnotations.forEach(annotation => {
|
||||||
|
const { mean, stdDev, max, area, unit } = annotation;
|
||||||
|
|
||||||
|
if (!mean || !unit || !max || !area) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(
|
||||||
|
`max (${unit})`,
|
||||||
|
`mean (${unit})`,
|
||||||
|
`std (${unit})`,
|
||||||
|
`area (mm2)`
|
||||||
|
);
|
||||||
|
values.push(max, mean, stdDev, area);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (FrameOfReferenceUID) {
|
||||||
|
columns.push('FrameOfReferenceUID');
|
||||||
|
values.push(FrameOfReferenceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points) {
|
||||||
|
columns.push('points');
|
||||||
|
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||||
|
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||||
|
// so that it can be used in the csv report
|
||||||
|
values.push(points.map(p => p.join(' ')).join(';'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayText(mappedAnnotations, displaySet) {
|
||||||
|
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayText = [];
|
||||||
|
|
||||||
|
// Area is the same for all series
|
||||||
|
const { area, SOPInstanceUID } = mappedAnnotations[0];
|
||||||
|
|
||||||
|
const instance = displaySet.images.find(
|
||||||
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let InstanceNumber;
|
||||||
|
if (instance) {
|
||||||
|
InstanceNumber = instance.InstanceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedArea = utils.roundNumber(area, 2);
|
||||||
|
displayText.push(`${roundedArea} mm<sup>2</sup>`);
|
||||||
|
|
||||||
|
// Todo: we need a better UI for displaying all these information
|
||||||
|
mappedAnnotations.forEach(mappedAnnotation => {
|
||||||
|
const { unit, max, SeriesNumber } = mappedAnnotation;
|
||||||
|
|
||||||
|
if (max) {
|
||||||
|
const roundedMax = utils.roundNumber(max, 2);
|
||||||
|
|
||||||
|
displayText.push(
|
||||||
|
InstanceNumber
|
||||||
|
? `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber} I:${InstanceNumber})`
|
||||||
|
: `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return displayText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EllipticalROI;
|
||||||
@ -1,100 +0,0 @@
|
|||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
|
||||||
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
|
||||||
|
|
||||||
const EllipticalRoi = {
|
|
||||||
toAnnotation: (measurement, definition) => {},
|
|
||||||
toMeasurement: (
|
|
||||||
csToolsAnnotation,
|
|
||||||
DisplaySetService,
|
|
||||||
getValueTypeFromToolType
|
|
||||||
) => {
|
|
||||||
const { element, measurementData } = csToolsAnnotation;
|
|
||||||
const tool =
|
|
||||||
csToolsAnnotation.toolType ||
|
|
||||||
csToolsAnnotation.toolName ||
|
|
||||||
measurementData.toolType;
|
|
||||||
|
|
||||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
|
||||||
|
|
||||||
if (!validToolType(tool)) {
|
|
||||||
throw new Error('Tool not supported');
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = getSOPInstanceAttributes(element);
|
|
||||||
|
|
||||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
|
||||||
SOPInstanceUID,
|
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
const { cachedStats, handles } = measurementData;
|
|
||||||
|
|
||||||
const { start, end } = handles;
|
|
||||||
|
|
||||||
const halfXLength = Math.abs(start.x - end.x) / 2;
|
|
||||||
const halfYLength = Math.abs(start.y - end.y) / 2;
|
|
||||||
|
|
||||||
const points = [];
|
|
||||||
const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 };
|
|
||||||
|
|
||||||
// To store similar to SR.
|
|
||||||
if (halfXLength > halfYLength) {
|
|
||||||
// X-axis major
|
|
||||||
// Major axis
|
|
||||||
points.push({ x: center.x - halfXLength, y: center.y });
|
|
||||||
points.push({ x: center.x + halfXLength, y: center.y });
|
|
||||||
// Minor axis
|
|
||||||
points.push({ x: center.x, y: center.y - halfYLength });
|
|
||||||
points.push({ x: center.x, y: center.y + halfYLength });
|
|
||||||
} else {
|
|
||||||
// Y-axis major
|
|
||||||
// Major axis
|
|
||||||
points.push({ x: center.x, y: center.y - halfYLength });
|
|
||||||
points.push({ x: center.x, y: center.y + halfYLength });
|
|
||||||
// Minor axis
|
|
||||||
points.push({ x: center.x - halfXLength, y: center.y });
|
|
||||||
points.push({ x: center.x + halfXLength, y: center.y });
|
|
||||||
}
|
|
||||||
|
|
||||||
let meanSUV;
|
|
||||||
let stdDevSUV;
|
|
||||||
|
|
||||||
if (
|
|
||||||
cachedStats &&
|
|
||||||
cachedStats.meanStdDevSUV &&
|
|
||||||
cachedStats.meanStdDevSUV.mean !== 0
|
|
||||||
) {
|
|
||||||
const { meanStdDevSUV } = cachedStats;
|
|
||||||
|
|
||||||
meanSUV = meanStdDevSUV.mean;
|
|
||||||
stdDevSUV = meanStdDevSUV.stdDev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: measurementData.id,
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
|
||||||
referenceStudyUID: StudyInstanceUID,
|
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
|
||||||
label: measurementData.label,
|
|
||||||
description: measurementData.description,
|
|
||||||
unit: measurementData.unit,
|
|
||||||
area: cachedStats && cachedStats.area,
|
|
||||||
mean: cachedStats && cachedStats.mean,
|
|
||||||
stdDev: cachedStats && cachedStats.stdDev,
|
|
||||||
meanSUV,
|
|
||||||
stdDevSUV,
|
|
||||||
type: getValueTypeFromToolType(tool),
|
|
||||||
points,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EllipticalRoi;
|
|
||||||
@ -1,10 +1,9 @@
|
|||||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||||
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
|
||||||
import getPointsFromHandles from './utils/getPointsFromHandles';
|
|
||||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||||
|
import { DisplaySetService, utils } from '@ohif/core';
|
||||||
|
|
||||||
const Length = {
|
const Length = {
|
||||||
toAnnotation: (measurement, definition) => {},
|
toAnnotation: measurement => {},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps cornerstone annotation event data to measurement service format.
|
* Maps cornerstone annotation event data to measurement service format.
|
||||||
@ -13,49 +12,188 @@ const Length = {
|
|||||||
* @return {Measurement} Measurement instance
|
* @return {Measurement} Measurement instance
|
||||||
*/
|
*/
|
||||||
toMeasurement: (
|
toMeasurement: (
|
||||||
csToolsAnnotation,
|
csToolsEventDetail,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
getValueTypeFromToolType
|
getValueTypeFromToolType
|
||||||
) => {
|
) => {
|
||||||
const { element, measurementData } = csToolsAnnotation;
|
const { annotation, viewportId } = csToolsEventDetail;
|
||||||
const tool =
|
const { metadata, data, annotationUID } = annotation;
|
||||||
csToolsAnnotation.toolType ||
|
|
||||||
csToolsAnnotation.toolName ||
|
|
||||||
measurementData.toolType;
|
|
||||||
|
|
||||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
if (!metadata || !data) {
|
||||||
|
console.warn('Length tool: Missing metadata or data');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!validToolType(tool)) {
|
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||||
|
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||||
|
|
||||||
|
if (!validToolType) {
|
||||||
throw new Error('Tool not supported');
|
throw new Error('Tool not supported');
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
} = getSOPInstanceAttributes(element);
|
} = getSOPInstanceAttributes(
|
||||||
|
referencedImageId,
|
||||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
CornerstoneViewportService,
|
||||||
SOPInstanceUID,
|
viewportId
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (SOPInstanceUID) {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { points } = data.handles;
|
||||||
|
|
||||||
|
const mappedAnnotations = getMappedAnnotations(
|
||||||
|
annotation,
|
||||||
|
DisplaySetService
|
||||||
|
);
|
||||||
|
|
||||||
|
const displayText = getDisplayText(mappedAnnotations, displaySet);
|
||||||
|
const getReport = () =>
|
||||||
|
_getReport(mappedAnnotations, points, FrameOfReferenceUID);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: measurementData.id,
|
uid: annotationUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
FrameOfReferenceUID,
|
FrameOfReferenceUID,
|
||||||
|
points,
|
||||||
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: measurementData.label,
|
label: data.label,
|
||||||
description: measurementData.description,
|
displayText: displayText,
|
||||||
unit: measurementData.unit,
|
data: data.cachedStats,
|
||||||
length: measurementData.length,
|
type: getValueTypeFromToolType(toolName),
|
||||||
type: getValueTypeFromToolType(tool),
|
getReport,
|
||||||
points: getPointsFromHandles(measurementData.handles),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getMappedAnnotations(annotation, DisplaySetService) {
|
||||||
|
const { metadata, data } = annotation;
|
||||||
|
const { cachedStats } = data;
|
||||||
|
const { referencedImageId } = metadata;
|
||||||
|
const targets = Object.keys(cachedStats);
|
||||||
|
|
||||||
|
if (!targets.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotations = [];
|
||||||
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (referencedImageId) {
|
||||||
|
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
||||||
|
referencedImageId
|
||||||
|
);
|
||||||
|
|
||||||
|
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
||||||
|
const { length } = targetStats;
|
||||||
|
const unit = 'mm';
|
||||||
|
|
||||||
|
annotations.push({
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SeriesNumber,
|
||||||
|
unit,
|
||||||
|
length,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This function is used to convert the measurement data to a format that is
|
||||||
|
suitable for the report generation (e.g. for the csv report). The report
|
||||||
|
returns a list of columns and corresponding values.
|
||||||
|
*/
|
||||||
|
function _getReport(mappedAnnotations, points, FrameOfReferenceUID) {
|
||||||
|
const columns = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
// Add Type
|
||||||
|
columns.push('AnnotationType');
|
||||||
|
values.push('Cornerstone:Length');
|
||||||
|
|
||||||
|
mappedAnnotations.forEach(annotation => {
|
||||||
|
const { length } = annotation;
|
||||||
|
columns.push(`Length (mm)`);
|
||||||
|
values.push(length);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (FrameOfReferenceUID) {
|
||||||
|
columns.push('FrameOfReferenceUID');
|
||||||
|
values.push(FrameOfReferenceUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points) {
|
||||||
|
columns.push('points');
|
||||||
|
// points has the form of [[x1, y1, z1], [x2, y2, z2], ...]
|
||||||
|
// convert it to string of [[x1 y1 z1];[x2 y2 z2];...]
|
||||||
|
// so that it can be used in the csv report
|
||||||
|
values.push(points.map(p => p.join(' ')).join(';'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayText(mappedAnnotations, displaySet) {
|
||||||
|
if (!mappedAnnotations || !mappedAnnotations.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayText = [];
|
||||||
|
|
||||||
|
// Area is the same for all series
|
||||||
|
const { length, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
||||||
|
|
||||||
|
const instance = displaySet.images.find(
|
||||||
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let InstanceNumber;
|
||||||
|
if (instance) {
|
||||||
|
InstanceNumber = instance.InstanceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedLength = utils.roundNumber(length, 2);
|
||||||
|
displayText.push(
|
||||||
|
InstanceNumber
|
||||||
|
? `${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
|
||||||
|
: `${roundedLength} mm (S: ${SeriesNumber})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return displayText;
|
||||||
|
}
|
||||||
|
|
||||||
export default Length;
|
export default Length;
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate'];
|
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import Length from './Length';
|
import Length from './Length';
|
||||||
import Bidirectional from './Bidirectional';
|
import Bidirectional from './Bidirectional';
|
||||||
|
import EllipticalROI from './EllipticalROI';
|
||||||
import ArrowAnnotate from './ArrowAnnotate';
|
import ArrowAnnotate from './ArrowAnnotate';
|
||||||
import EllipticalRoi from './EllipticalRoi';
|
|
||||||
|
|
||||||
const measurementServiceMappingsFactory = (
|
const measurementServiceMappingsFactory = (
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService
|
||||||
) => {
|
) => {
|
||||||
/**
|
/**
|
||||||
* Maps measurement service format object to cornerstone annotation object.
|
* Maps measurement service format object to cornerstone annotation object.
|
||||||
@ -19,16 +20,18 @@ const measurementServiceMappingsFactory = (
|
|||||||
const {
|
const {
|
||||||
POLYLINE,
|
POLYLINE,
|
||||||
ELLIPSE,
|
ELLIPSE,
|
||||||
POINT,
|
RECTANGLE,
|
||||||
BIDIRECTIONAL,
|
BIDIRECTIONAL,
|
||||||
|
POINT,
|
||||||
} = MeasurementService.VALUE_TYPES;
|
} = MeasurementService.VALUE_TYPES;
|
||||||
|
|
||||||
// TODO -> I get why this was attemped, but its not nearly flexible enough.
|
// TODO -> I get why this was attempted, but its not nearly flexible enough.
|
||||||
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
|
||||||
// You can't define a bidirectional tool as a single type..
|
// You can't define a bidirectional tool as a single type..
|
||||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||||
Length: POLYLINE,
|
Length: POLYLINE,
|
||||||
EllipticalRoi: ELLIPSE,
|
EllipticalROI: ELLIPSE,
|
||||||
|
RectangleROI: RECTANGLE,
|
||||||
Bidirectional: BIDIRECTIONAL,
|
Bidirectional: BIDIRECTIONAL,
|
||||||
ArrowAnnotate: POINT,
|
ArrowAnnotate: POINT,
|
||||||
};
|
};
|
||||||
@ -43,6 +46,7 @@ const measurementServiceMappingsFactory = (
|
|||||||
Length.toMeasurement(
|
Length.toMeasurement(
|
||||||
csToolsAnnotation,
|
csToolsAnnotation,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
),
|
),
|
||||||
matchingCriteria: [
|
matchingCriteria: [
|
||||||
@ -58,6 +62,7 @@ const measurementServiceMappingsFactory = (
|
|||||||
Bidirectional.toMeasurement(
|
Bidirectional.toMeasurement(
|
||||||
csToolsAnnotation,
|
csToolsAnnotation,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
),
|
),
|
||||||
matchingCriteria: [
|
matchingCriteria: [
|
||||||
@ -73,12 +78,28 @@ const measurementServiceMappingsFactory = (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
EllipticalROI: {
|
||||||
|
toAnnotation: EllipticalROI.toAnnotation,
|
||||||
|
toMeasurement: csToolsAnnotation =>
|
||||||
|
EllipticalROI.toMeasurement(
|
||||||
|
csToolsAnnotation,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
_getValueTypeFromToolType
|
||||||
|
),
|
||||||
|
matchingCriteria: [
|
||||||
|
{
|
||||||
|
valueType: MeasurementService.VALUE_TYPES.ELLIPSE,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
ArrowAnnotate: {
|
ArrowAnnotate: {
|
||||||
toAnnotation: ArrowAnnotate.toAnnotation,
|
toAnnotation: ArrowAnnotate.toAnnotation,
|
||||||
toMeasurement: csToolsAnnotation =>
|
toMeasurement: csToolsAnnotation =>
|
||||||
ArrowAnnotate.toMeasurement(
|
ArrowAnnotate.toMeasurement(
|
||||||
csToolsAnnotation,
|
csToolsAnnotation,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
),
|
),
|
||||||
matchingCriteria: [
|
matchingCriteria: [
|
||||||
@ -88,20 +109,6 @@ const measurementServiceMappingsFactory = (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
EllipticalRoi: {
|
|
||||||
toAnnotation: EllipticalRoi.toAnnotation,
|
|
||||||
toMeasurement: csToolsAnnotation =>
|
|
||||||
EllipticalRoi.toMeasurement(
|
|
||||||
csToolsAnnotation,
|
|
||||||
DisplaySetService,
|
|
||||||
_getValueTypeFromToolType
|
|
||||||
),
|
|
||||||
matchingCriteria: [
|
|
||||||
{
|
|
||||||
valueType: MeasurementService.VALUE_TYPES.ELLIPSE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';
|
|
||||||
|
|
||||||
jest.mock('cornerstone-core', () => ({
|
|
||||||
...jest.requireActual('cornerstone-core'),
|
|
||||||
getEnabledElement: () => ({
|
|
||||||
image: { imageId: 123 },
|
|
||||||
}),
|
|
||||||
metaData: {
|
|
||||||
...jest.requireActual('cornerstone-core').metaData,
|
|
||||||
get: () => ({
|
|
||||||
SOPInstanceUID: '123',
|
|
||||||
FrameOfReferenceUID: '123',
|
|
||||||
SeriesInstanceUID: '123',
|
|
||||||
StudyInstanceUID: '1234',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('measurementServiceMappings.js', () => {
|
|
||||||
let mappings;
|
|
||||||
let handles;
|
|
||||||
let points;
|
|
||||||
let csToolsAnnotation;
|
|
||||||
let measurement;
|
|
||||||
let measurementServiceMock;
|
|
||||||
let displaySetServiceMock;
|
|
||||||
let definition = 'Length';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
measurementServiceMock = {
|
|
||||||
VALUE_TYPES: {
|
|
||||||
POLYLINE: 'value_type::polyline',
|
|
||||||
POINT: 'value_type::point',
|
|
||||||
ELLIPSE: 'value_type::ellipse',
|
|
||||||
MULTIPOINT: 'value_type::multipoint',
|
|
||||||
CIRCLE: 'value_type::circle',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
displaySetServiceMock = {
|
|
||||||
getDisplaySetForSOPInstanceUID: (SOPInstanceUID, SeriesInstanceUID) => {
|
|
||||||
console.warn('SOPInstanceUID');
|
|
||||||
|
|
||||||
return {
|
|
||||||
displaySetInstanceUID: '1.2.3.4'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mappings = measurementServiceMappingsFactory(measurementServiceMock, displaySetServiceMock);
|
|
||||||
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
|
|
||||||
points = [
|
|
||||||
{ x: 1, y: 2 },
|
|
||||||
{ x: 1, y: 2 },
|
|
||||||
];
|
|
||||||
csToolsAnnotation = {
|
|
||||||
toolName: definition,
|
|
||||||
measurementData: {
|
|
||||||
id: 1,
|
|
||||||
label: 'Test',
|
|
||||||
SOPInstanceUID: '123',
|
|
||||||
FrameOfReferenceUID: '123',
|
|
||||||
SeriesInstanceUID: '123',
|
|
||||||
handles,
|
|
||||||
text: 'Test',
|
|
||||||
description: 'Test',
|
|
||||||
unit: 'mm',
|
|
||||||
length: undefined
|
|
||||||
},
|
|
||||||
};
|
|
||||||
measurement = {
|
|
||||||
id: 1,
|
|
||||||
SOPInstanceUID: '123',
|
|
||||||
FrameOfReferenceUID: '123',
|
|
||||||
referenceSeriesUID: '123',
|
|
||||||
referenceStudyUID: '1234',
|
|
||||||
displaySetInstanceUID: '1.2.3.4',
|
|
||||||
label: 'Test',
|
|
||||||
description: 'Test',
|
|
||||||
unit: 'mm',
|
|
||||||
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
|
|
||||||
points,
|
|
||||||
};
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
/*describe('toAnnotation()', () => {
|
|
||||||
it('map measurement service format to annotation', async () => {
|
|
||||||
const mappedMeasurement = await mappings[csToolsAnnotation.toolName].toAnnotation(
|
|
||||||
measurement,
|
|
||||||
definition
|
|
||||||
);
|
|
||||||
expect(mappedMeasurement).toEqual(csToolsAnnotation);
|
|
||||||
});
|
|
||||||
});*/
|
|
||||||
|
|
||||||
describe('toMeasurement()', () => {
|
|
||||||
it('map annotation to measurement service format', async () => {
|
|
||||||
const getValueTypeFromToolType = (toolType) => 'valueType';
|
|
||||||
const mappedAnnotation = await mappings[csToolsAnnotation.toolName].toMeasurement(csToolsAnnotation, displaySetServiceMock, getValueTypeFromToolType);
|
|
||||||
expect(mappedAnnotation).toEqual(measurement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
function getModalityUnit(modality) {
|
||||||
|
if (modality === 'CT') {
|
||||||
|
return 'HU';
|
||||||
|
} else if (modality === 'PT') {
|
||||||
|
return 'SUV';
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getModalityUnit;
|
||||||
@ -1,12 +0,0 @@
|
|||||||
export default function getPointsFromHandles(handles) {
|
|
||||||
let points = [];
|
|
||||||
Object.keys(handles).map(handle => {
|
|
||||||
if (['start', 'end'].includes(handle)) {
|
|
||||||
let point = {};
|
|
||||||
if (handles[handle].x) point.x = handles[handle].x;
|
|
||||||
if (handles[handle].y) point.y = handles[handle].y;
|
|
||||||
points.push(point);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user