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": [
|
||||
"react-app",
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
|
||||
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@ -11,5 +11,18 @@
|
||||
"url": "http://localhost:3000",
|
||||
"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 =
|
||||
mode === 'production'
|
||||
? excludeNodeModulesExcept([
|
||||
'vtk.js',
|
||||
// 'dicomweb-client',
|
||||
// https://github.com/react-dnd/react-dnd/blob/master/babel.config.js
|
||||
'react-dnd',
|
||||
@ -21,7 +20,8 @@ function transpileJavaScript(mode) {
|
||||
: excludeNodeModulesExcept([]);
|
||||
|
||||
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
|
||||
// JS version (currently ES5). Most of these leverage ES6+ features,
|
||||
// that we need to transpile to a different syntax.
|
||||
|
||||
@ -12,6 +12,8 @@ const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
// ~~ PackageJSON
|
||||
const PACKAGE = require('../platform/viewer/package.json');
|
||||
// const vtkRules = require('vtk.js/Utilities/config/dependency.js').webpack.core
|
||||
// .rules;
|
||||
// ~~ RULES
|
||||
const loadShadersRule = require('./rules/loadShaders.js');
|
||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||
@ -66,11 +68,20 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
devServer: {
|
||||
open: true,
|
||||
port: 3000,
|
||||
historyApiFallback: true,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
transpileJavaScriptRule(mode),
|
||||
loadWebWorkersRule,
|
||||
loadShadersRule,
|
||||
// loadShadersRule,
|
||||
{
|
||||
test: /\.m?js/,
|
||||
resolve: {
|
||||
@ -78,7 +89,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
},
|
||||
},
|
||||
cssToJavaScript,
|
||||
],
|
||||
], //.concat(vtkRules),
|
||||
},
|
||||
resolve: {
|
||||
mainFields: ['module', 'browser', 'main'],
|
||||
@ -106,7 +117,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
SRC_DIR,
|
||||
],
|
||||
// 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
|
||||
symlinks: true,
|
||||
fallback: { fs: false, path: false, zlib: false },
|
||||
|
||||
@ -190,13 +190,14 @@ you'll see the following:
|
||||
├── extensions #
|
||||
│ ├── _example # Skeleton of example extension
|
||||
│ ├── default #
|
||||
│ ├── cornerstone # 2D images w/ Cornerstone.js
|
||||
│ ├── dicom-sr #
|
||||
│ ├── cornerstone # image rendering and tools w/ Cornerstone
|
||||
│ ├── cornerstone- dicom-sr #
|
||||
│ └── measurement-tracking #
|
||||
│
|
||||
├── modes #
|
||||
│ ├── _example # Skeleton of example mode
|
||||
│ └── longitudinal #
|
||||
│ ├── basic-dev-mode # Basic development mode
|
||||
│ └── longitudinal # Longitudinal mode (measurement tracking)
|
||||
│
|
||||
├── platform #
|
||||
│ ├── core # Business Logic
|
||||
|
||||
@ -1,10 +1,16 @@
|
||||
const aliases = require('./aliases.config');
|
||||
const path = require('path');
|
||||
|
||||
// https://babeljs.io/docs/en/options#babelrcroots
|
||||
module.exports = {
|
||||
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: {
|
||||
test: {
|
||||
presets: [
|
||||
@ -17,12 +23,14 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-typescript',
|
||||
],
|
||||
},
|
||||
production: {
|
||||
@ -30,6 +38,7 @@ module.exports = {
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
@ -38,6 +47,7 @@ module.exports = {
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
|
||||
@ -14,7 +14,6 @@ module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-sr",
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.0.0",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"module": "src/index.tsx",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
@ -34,21 +34,18 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"cornerstone-core": "^2.6.0",
|
||||
"cornerstone-math": "^0.1.9",
|
||||
"cornerstone-tools": "6.0.2",
|
||||
"cornerstone-wado-image-loader": "4.0.4",
|
||||
"dcmjs": "0.16.1",
|
||||
"dcmjs": "^0.24.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-cornerstone-viewport": "4.1.2",
|
||||
"@ohif/extension-cornerstone": "^3.0.0",
|
||||
"@ohif/extension-measurement-tracking": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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 addMeasurement from './utils/addMeasurement';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import { adapters } from 'dcmjs';
|
||||
|
||||
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D;
|
||||
|
||||
const { ImageSet } = classes;
|
||||
|
||||
// TODO ->
|
||||
// Add SR thumbnail
|
||||
// Make viewport
|
||||
@ -16,6 +18,9 @@ const sopClassUids = [
|
||||
'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 = {
|
||||
ImagingMeasurementReport: '126000',
|
||||
ImageLibrary: '111028',
|
||||
@ -26,12 +31,15 @@ const CodeNameCodeSequenceValues = {
|
||||
TrackingIdentifier: '112039',
|
||||
Finding: '121071',
|
||||
FindingSite: 'G-C0E3', // SRT
|
||||
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
|
||||
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
|
||||
};
|
||||
|
||||
const CodingSchemeDesignators = {
|
||||
SRT: 'SRT',
|
||||
cornerstoneTools4: 'CST4',
|
||||
CornerstoneCodeSchemes: [
|
||||
Cornerstone3DCodeScheme.CodingSchemeDesignator,
|
||||
'CST4',
|
||||
],
|
||||
};
|
||||
|
||||
const RELATIONSHIP_TYPE = {
|
||||
@ -117,8 +125,8 @@ function _load(displaySet, servicesManager, extensionManager) {
|
||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||
|
||||
const mappings = MeasurementService.getSourceMappings(
|
||||
'CornerstoneTools',
|
||||
'4'
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||
);
|
||||
|
||||
displaySet.isHydrated = false;
|
||||
@ -217,7 +225,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
|
||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||
const measurement = unloadedMeasurements[j];
|
||||
if (_measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
|
||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
|
||||
addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
@ -231,7 +239,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
}
|
||||
}
|
||||
|
||||
function _measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
|
||||
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
|
||||
const { coords } = measurement;
|
||||
|
||||
for (let j = 0; j < coords.length; j++) {
|
||||
@ -458,8 +466,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
|
||||
if (
|
||||
Finding &&
|
||||
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
Finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
Finding.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
) {
|
||||
@ -473,8 +482,9 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
if (FindingSites.length) {
|
||||
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
||||
FindingSite =>
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
);
|
||||
@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import onModeEnter from './onModeEnter';
|
||||
import commandsModule from './commandsModule';
|
||||
import init from './init';
|
||||
import { id } from './id.js';
|
||||
import toolNames from './tools/toolNames';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -21,11 +23,12 @@ const OHIFCornerstoneSRViewport = props => {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
const dicomSRExtension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
onModeEnter,
|
||||
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
@ -50,50 +53,24 @@ export default {
|
||||
|
||||
return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }];
|
||||
},
|
||||
getCommandsModule({ servicesManager }) {
|
||||
return {
|
||||
definitions: {
|
||||
setToolActive: {
|
||||
commandFn: ({ toolName, element }) => {
|
||||
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',
|
||||
};
|
||||
getCommandsModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
getSopClassHandlerModule,
|
||||
onModeEnter,
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
name: 'tools',
|
||||
exports: {
|
||||
toolNames,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
function _getToolAlias(toolName) {
|
||||
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;
|
||||
}
|
||||
export default dicomSRExtension;
|
||||
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';
|
||||
|
||||
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`.
|
||||
@ -14,7 +17,7 @@ export default function isRehydratable(displaySet, mappings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mappingDefinitions = mappings.map(m => m.definition);
|
||||
const mappingDefinitions = mappings.map(m => m.annotationType);
|
||||
const { measurements } = displaySet;
|
||||
|
||||
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||
@ -35,9 +38,18 @@ export default function isRehydratable(displaySet, mappings) {
|
||||
|
||||
for (let i = 0; i < measurements.length; i++) {
|
||||
const TrackingIdentifier = measurements[i].TrackingIdentifier;
|
||||
const hydratable = adapters.some(adapter =>
|
||||
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
|
||||
);
|
||||
const hydratable = adapters.some(adapter => {
|
||||
let [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
if (supportedLegacyCornerstoneTags.includes(cornerstoneTag)) {
|
||||
cornerstoneTag = CORNERSTONE_3D_TAG;
|
||||
}
|
||||
|
||||
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
|
||||
|
||||
return adapter.isValidCornerstoneTrackingIdentifier(
|
||||
mappedTrackingIdentifier
|
||||
);
|
||||
});
|
||||
|
||||
if (hydratable) {
|
||||
return true;
|
||||
@ -7,7 +7,7 @@ import dcmjs from 'dcmjs';
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isToolSupported = toolName => {
|
||||
const adapter = dcmjs.adapters.Cornerstone;
|
||||
const adapter = dcmjs.adapters.Cornerstone3D;
|
||||
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 webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
const webpack = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
@ -13,7 +12,6 @@ module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
@ -40,10 +38,6 @@ module.exports = (env, argv) => {
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
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
|
||||
|
||||

|
||||
|
||||
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 -->
|
||||
```
|
||||
# Cornerstone
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"module": "src/index.tsx",
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
@ -16,9 +16,6 @@
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"keywords": [
|
||||
"ohif-extension"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@ -27,29 +24,33 @@
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"cornerstone-core": "2.6.0",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"cornerstone-tools": "6.0.2",
|
||||
"cornerstone-wado-image-loader": "4.0.4",
|
||||
"dcmjs": "0.16.1",
|
||||
"cornerstone-wado-image-loader": "^4.2.0",
|
||||
"dcmjs": "^0.24.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^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": {
|
||||
"@babel/runtime": "7.16.3",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"@babel/runtime": "7.17.9",
|
||||
"@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",
|
||||
"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 cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
import {
|
||||
getEnabledElement,
|
||||
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 } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
||||
import callInputDialog from './utils/callInputDialog';
|
||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
const {
|
||||
ViewportGridService,
|
||||
ToolGroupService,
|
||||
CineService,
|
||||
ToolBarService,
|
||||
UIDialogService,
|
||||
CornerstoneViewportService,
|
||||
SegmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const imagePositionSynchronizer = new cornerstoneTools.Synchronizer(
|
||||
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() {
|
||||
function _getActiveViewportEnabledElement() {
|
||||
const { activeViewportIndex } = ViewportGridService.getState();
|
||||
const { element } = getEnabledElement(activeViewportIndex) || {};
|
||||
return element;
|
||||
const { element } = OHIFgetEnabledElement(activeViewportIndex) || {};
|
||||
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 = {
|
||||
getCornerstoneLibraries: () => {
|
||||
return { cornerstone, cornerstoneTools };
|
||||
getActiveViewportEnabledElement: () => {
|
||||
return _getActiveViewportEnabledElement();
|
||||
},
|
||||
rotateViewport: ({ rotation }) => {
|
||||
const enabledElement = _getActiveViewportsEnabledElement();
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.rotation += rotation;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
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();
|
||||
setViewportActive: ({ viewportId }) => {
|
||||
const viewportInfo = CornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
);
|
||||
if (!viewportInfo) {
|
||||
console.warn('No viewport found for viewportId:', viewportId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Erase existing state and then set up all currently existing elements
|
||||
cornerstone.getEnabledElements().map(e => {
|
||||
synchronizers.forEach(s => {
|
||||
s.remove(e.element);
|
||||
s.add(e.element);
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
ViewportGridService.setActiveViewportIndex(viewportIndex);
|
||||
},
|
||||
arrowTextCallback: ({ callback, data }) => {
|
||||
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) {
|
||||
enabledElement = _getActiveViewportsEnabledElement();
|
||||
} else {
|
||||
enabledElement = element;
|
||||
}
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
viewport.render();
|
||||
}
|
||||
},
|
||||
cancelMeasurement: () => {
|
||||
const enabledElement = _getActiveViewportsEnabledElement();
|
||||
toggleCrosshairs({ toolGroupId, toggledState }) {
|
||||
const toolName = 'Crosshairs';
|
||||
// If it is Enabled
|
||||
if (toggledState) {
|
||||
actions.setToolActive({ toolName, toolGroupId });
|
||||
return;
|
||||
}
|
||||
const toolGroup = _getToolGroup(toolGroupId);
|
||||
|
||||
if (enabledElement) {
|
||||
const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||
'manipulatorState'
|
||||
).setters.cancelActiveManipulatorsForElement;
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
// better mouseButtonMask sets.
|
||||
setToolActive: ({ toolName }) => {
|
||||
if (!toolName) {
|
||||
console.warn('No toolname provided to setToolActive command');
|
||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||
const toolGroup = _getToolGroup(toolGroupId);
|
||||
|
||||
if (!toolGroup) {
|
||||
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() || { viewports: [] };
|
||||
for (let i = 0; i < viewports.length; i++) {
|
||||
const viewport = viewports[i];
|
||||
const hasDisplaySet = viewport.displaySetInstanceUID !== undefined;
|
||||
const { viewports } = ViewportGridService.getState() || {
|
||||
viewports: [],
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const viewportInfo = getEnabledElement(i);
|
||||
if (!viewportInfo) continue;
|
||||
const hasCornerstoneContext =
|
||||
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
|
||||
const viewport = getEnabledElement(ohifEnabledElement.element);
|
||||
|
||||
if (hasCornerstoneContext) {
|
||||
cornerstoneTools.setToolActiveForElement(
|
||||
viewportInfo.element,
|
||||
toolName,
|
||||
{ mouseButtonMask: 1 }
|
||||
);
|
||||
} else {
|
||||
commandsManager.runCommand(
|
||||
'setToolActive',
|
||||
{
|
||||
element: viewportInfo.element,
|
||||
toolName,
|
||||
},
|
||||
viewportInfo.context
|
||||
);
|
||||
if (!viewport) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
clearAnnotations: () => {
|
||||
const element = _getActiveViewportsEnabledElement();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { enabledElement } = cornerstone.getEnabledElement(element) || {};
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return;
|
||||
}
|
||||
// Find the current active tool and set it to be passive
|
||||
const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
|
||||
|
||||
const {
|
||||
toolState,
|
||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
if (
|
||||
!toolState ||
|
||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (activeTool) {
|
||||
toolGroup.setToolPassive(activeTool);
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||
|
||||
const measurementsToRemove = [];
|
||||
|
||||
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,
|
||||
});
|
||||
// Set the new toolName to be active
|
||||
toolGroup.setToolActive(toolName, {
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
});
|
||||
});
|
||||
|
||||
measurementsToRemove.forEach(measurementData => {
|
||||
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;
|
||||
return;
|
||||
}
|
||||
},
|
||||
showDownloadViewportModal: () => {
|
||||
const { activeViewportIndex } = ViewportGridService.getState();
|
||||
@ -288,119 +211,215 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
CornerstoneViewportService,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
|
||||
const nearbyTool = {};
|
||||
let pointNearTool = false;
|
||||
rotateViewport: ({ rotation }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
if (!enabledElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
availableToolTypes.forEach(toolType => {
|
||||
const elementToolData = cornerstoneTools.getToolState(
|
||||
element,
|
||||
toolType
|
||||
);
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (!elementToolData) {
|
||||
return;
|
||||
if (viewport instanceof StackViewport) {
|
||||
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) => {
|
||||
let elementToolInstance = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
toolType
|
||||
);
|
||||
if (!enabledElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elementToolInstance) {
|
||||
elementToolInstance = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
`${toolType}Tool`
|
||||
);
|
||||
}
|
||||
const { viewport } = enabledElement;
|
||||
const options = { delta: direction };
|
||||
|
||||
if (!elementToolInstance) {
|
||||
console.warn('Tool not found.');
|
||||
return undefined;
|
||||
}
|
||||
csToolsUtils.scroll(viewport, options);
|
||||
},
|
||||
async createSegmentationForDisplaySet({ displaySetInstanceUID }) {
|
||||
const volumeId = displaySetInstanceUID;
|
||||
|
||||
if (
|
||||
elementToolInstance.pointNearTool(
|
||||
element,
|
||||
toolData,
|
||||
canvasCoordinates
|
||||
)
|
||||
) {
|
||||
pointNearTool = true;
|
||||
nearbyTool.tool = toolData;
|
||||
nearbyTool.index = index;
|
||||
nearbyTool.toolType = toolType;
|
||||
}
|
||||
});
|
||||
const segmentationUID = csUtils.uuidv4();
|
||||
const segmentationId = `${volumeId}::${segmentationUID}`;
|
||||
|
||||
if (pointNearTool) {
|
||||
return false;
|
||||
}
|
||||
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
|
||||
volumeId: segmentationId,
|
||||
});
|
||||
|
||||
return pointNearTool ? nearbyTool : undefined;
|
||||
},
|
||||
removeToolState: ({ element, toolType, tool }) => {
|
||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||
cornerstone.updateImage(element);
|
||||
},
|
||||
// setCornerstoneLayout: () => {
|
||||
// setCornerstoneLayout();
|
||||
// },
|
||||
setWindowLevel: ({ window, level }) => {
|
||||
const enabledElement = _getActiveViewportsEnabledElement();
|
||||
// Add the segmentations to state
|
||||
segmentation.addSegmentations([
|
||||
{
|
||||
segmentationId,
|
||||
representation: {
|
||||
// The type of segmentation
|
||||
type: Enums.SegmentationRepresentations.Labelmap,
|
||||
// The actual segmentation data, in the case of labelmap this is a
|
||||
// reference to the source volume of the segmentation.
|
||||
data: {
|
||||
volumeId: segmentationId,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
return segmentationId;
|
||||
},
|
||||
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 = {
|
||||
windowWidth: Number(window),
|
||||
windowCenter: Number(level),
|
||||
};
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
const labelmapVolumes = segmentations.map(segmentation => {
|
||||
return cache.getVolume(segmentation.id);
|
||||
});
|
||||
|
||||
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 = {
|
||||
jumpToImage: {
|
||||
commandFn: actions.jumpToImage,
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getCornerstoneLibraries: {
|
||||
commandFn: actions.getCornerstoneLibraries,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleSynchronizer: {
|
||||
commandFn: actions.toggleSynchronizer,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
removeToolState: {
|
||||
commandFn: actions.removeToolState,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getActiveViewportEnabledElement: {
|
||||
commandFn: actions.getActiveViewportEnabledElement,
|
||||
toggleCrosshairs: {
|
||||
commandFn: actions.toggleCrosshairs,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
@ -414,13 +433,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
storeContexts: [],
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
cancelMeasurement: {
|
||||
commandFn: actions.cancelMeasurement,
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
@ -429,8 +443,13 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
@ -449,45 +468,54 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
storeContexts: [],
|
||||
options: { direction: 0 },
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
clearAnnotations: {
|
||||
commandFn: actions.clearAnnotations,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
nextImage: {
|
||||
commandFn: actions.nextImage,
|
||||
commandFn: actions.scroll,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousImage: {
|
||||
commandFn: actions.previousImage,
|
||||
commandFn: actions.scroll,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
toggleCine: {
|
||||
commandFn: actions.toggleCine,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setZoomTool: {
|
||||
commandFn: actions.setToolActive,
|
||||
arrowTextCallback: {
|
||||
commandFn: actions.arrowTextCallback,
|
||||
storeContexts: [],
|
||||
options: { toolName: 'Zoom' },
|
||||
options: {},
|
||||
},
|
||||
// setCornerstoneLayout: {
|
||||
// commandFn: actions.setCornerstoneLayout,
|
||||
// storeContexts: [],
|
||||
// options: {},
|
||||
// context: 'VIEWER',
|
||||
// },
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
setViewportActive: {
|
||||
commandFn: actions.setViewportActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createSegmentationForDisplaySet: {
|
||||
commandFn: actions.createSegmentationForDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
addSegmentationRepresentationToToolGroup: {
|
||||
commandFn: actions.addSegmentationRepresentationToToolGroup,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
getLabelmapVolumes: {
|
||||
commandFn: actions.getLabelmapVolumes,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setViewportColormap: {
|
||||
commandFn: actions.setViewportColormap,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
@ -496,7 +524,7 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
return {
|
||||
actions,
|
||||
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 { 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;
|
||||
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,
|
||||
commandsManager,
|
||||
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 {
|
||||
UIDialogService,
|
||||
UserAuthenticationService,
|
||||
ToolGroupService,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
ToolBarService,
|
||||
UserAuthenticationService,
|
||||
UIDialogService,
|
||||
CineService,
|
||||
CornerstoneViewportService,
|
||||
HangingProtocolService,
|
||||
SegmentationService,
|
||||
} = 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 */
|
||||
const measurementServiceSource = _connectToolsToMeasurementService(
|
||||
const measurementServiceSource = connectToolsToMeasurementService(
|
||||
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 => {
|
||||
if (!UIDialogService) {
|
||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||
@ -89,16 +129,17 @@ export default function init({
|
||||
|
||||
const onGetMenuItems = defaultMenuItems => {
|
||||
const { element, currentPoints } = event.detail;
|
||||
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
|
||||
|
||||
const nearbyToolData = utilities.getAnnotationNearPoint(
|
||||
element,
|
||||
canvasCoordinates: currentPoints.canvas,
|
||||
availableToolTypes: TOOL_TYPES_WITH_CONTEXT_MENU,
|
||||
});
|
||||
currentPoints.canvas
|
||||
);
|
||||
|
||||
let menuItems = [];
|
||||
if (nearbyToolData) {
|
||||
defaultMenuItems.forEach(item => {
|
||||
item.value = nearbyToolData;
|
||||
item.element = element;
|
||||
menuItems.push(item);
|
||||
});
|
||||
}
|
||||
@ -123,41 +164,15 @@ export default function init({
|
||||
onGetMenuItems,
|
||||
eventData: event.detail,
|
||||
onDelete: item => {
|
||||
const { tool: measurementData, toolType } = item.value;
|
||||
const { annotationUID } = item.value;
|
||||
|
||||
const uid = annotationUID;
|
||||
// Sync'd w/ Measurement Service
|
||||
if (measurementData.id) {
|
||||
measurementServiceSource.remove(measurementData.id);
|
||||
}
|
||||
// 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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
if (uid) {
|
||||
measurementServiceSource.remove(uid, {
|
||||
element: item.element,
|
||||
});
|
||||
console.log(csTools.store.state);
|
||||
}
|
||||
_refreshViewports();
|
||||
CONTEXT_MENU_OPEN = false;
|
||||
},
|
||||
onClose: () => {
|
||||
@ -165,11 +180,9 @@ export default function init({
|
||||
UIDialogService.dismiss({ id: 'context-menu' });
|
||||
},
|
||||
onSetLabel: item => {
|
||||
const { tool: measurementData } = item.value;
|
||||
const { annotationUID } = item.value;
|
||||
|
||||
const measurement = MeasurementService.getMeasurement(
|
||||
measurementData.id
|
||||
);
|
||||
const measurement = MeasurementService.getMeasurement(annotationUID);
|
||||
|
||||
callInputDialog(
|
||||
UIDialogService,
|
||||
@ -184,7 +197,7 @@ export default function init({
|
||||
});
|
||||
|
||||
MeasurementService.update(
|
||||
updatedMeasurement.id,
|
||||
updatedMeasurement.uid,
|
||||
updatedMeasurement,
|
||||
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 = () => {
|
||||
if (!UIDialogService) {
|
||||
console.warn('Unable to show dialog; no UI Dialog Service available.');
|
||||
@ -222,6 +222,24 @@ export default function init({
|
||||
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`
|
||||
* Need to fallback to event.which;
|
||||
@ -235,445 +253,50 @@ export default function init({
|
||||
clickMethodHandler(evt);
|
||||
};
|
||||
|
||||
const cancelContextMenuIfOpen = evt => {
|
||||
if (CONTEXT_MENU_OPEN) {
|
||||
resetContextMenu();
|
||||
}
|
||||
};
|
||||
// const cancelContextMenuIfOpen = evt => {
|
||||
// if (CONTEXT_MENU_OPEN) {
|
||||
// resetContextMenu();
|
||||
// }
|
||||
// };
|
||||
|
||||
// TODO: This is the handler for ALL ENABLED ELEMENT EVENTS
|
||||
// ... Activation logic should take place per element, not for all (diff behavior per ext)
|
||||
function elementEnabledHandler(tools, evt) {
|
||||
const element = evt.detail.element;
|
||||
function elementEnabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
_addConfiguredToolsForElement(
|
||||
UIDialogService,
|
||||
element,
|
||||
tools,
|
||||
configuration
|
||||
);
|
||||
|
||||
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||
element.addEventListener(
|
||||
csTools.EVENTS.MOUSE_CLICK,
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
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) {
|
||||
const element = evt.detail.element;
|
||||
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||
const { viewportId, element } = evt.detail;
|
||||
|
||||
const viewportInfo = CornerstoneViewportService.getViewportInfo(viewportId);
|
||||
ToolGroupService.disable(viewportInfo);
|
||||
|
||||
element.removeEventListener(
|
||||
csTools.EVENTS.MOUSE_CLICK,
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
contextMenuHandleClick
|
||||
);
|
||||
element.removeEventListener(cs.EVENTS.NEW_IMAGE, cancelContextMenuIfOpen);
|
||||
}
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
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();
|
||||
eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
||||
const { element } = evt.detail;
|
||||
utilities.stackPrefetch.disable(element);
|
||||
});
|
||||
|
||||
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 cornerstone from 'cornerstone-core';
|
||||
import cornerstoneMath from 'cornerstone-math';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
import {
|
||||
PanTool,
|
||||
WindowLevelTool,
|
||||
StackScrollTool,
|
||||
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 = {}) {
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
window.cornerstone = cornerstone;
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
textBoxFontSize: '15px',
|
||||
lineWidth: '1.5',
|
||||
};
|
||||
|
||||
// For debugging
|
||||
window.cornerstoneTools = cornerstoneTools;
|
||||
|
||||
cornerstoneTools.external.cornerstone = cornerstone;
|
||||
cornerstoneTools.external.Hammer = Hammer;
|
||||
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 defaultStyles = annotation.config.style.getDefaultToolStyles();
|
||||
annotation.config.style.setDefaultToolStyles({
|
||||
global: {
|
||||
...defaultStyles.global,
|
||||
...annotationStyle,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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 dicomParser from 'dicom-parser';
|
||||
import { errorHandler } from '@ohif/core';
|
||||
|
||||
const { registerVolumeLoader } = volumeLoader;
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function initWebWorkers() {
|
||||
function initWebWorkers(appConfig) {
|
||||
const config = {
|
||||
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||
maxWebWorkers: Math.min(
|
||||
Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||
appConfig.maxNumberOfWebWorkers
|
||||
),
|
||||
startWebWorkersOnDemand: true,
|
||||
taskConfiguration: {
|
||||
decodeTask: {
|
||||
@ -24,11 +31,27 @@ function initWebWorkers() {
|
||||
}
|
||||
}
|
||||
|
||||
export default function initWADOImageLoader(UserAuthenticationService) {
|
||||
export default function initWADOImageLoader(
|
||||
UserAuthenticationService,
|
||||
appConfig
|
||||
) {
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||
|
||||
registerVolumeLoader(
|
||||
'cornerstoneStreamingImageVolume',
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
|
||||
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) {
|
||||
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
|
||||
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
|
||||
const xhrRequestHeaders = {
|
||||
// To prevent Preflight requests:
|
||||
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=*',
|
||||
};
|
||||
|
||||
@ -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 = {
|
||||
// The `defaultContext` of an extension's commandsModule
|
||||
DEFAULT_CONTEXT: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
DEFAULT_CONTEXT: 'CORNERSTONE',
|
||||
enabledElements: {},
|
||||
};
|
||||
|
||||
@ -9,7 +9,11 @@ const state = {
|
||||
* @param {HTMLElement} dom Active viewport element.
|
||||
* @return void
|
||||
*/
|
||||
const setEnabledElement = (viewportIndex, element, context) => {
|
||||
const setEnabledElement = (
|
||||
viewportIndex: number,
|
||||
element: HTMLElement,
|
||||
context?: string
|
||||
): void => {
|
||||
const targetContext = context || state.DEFAULT_CONTEXT;
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -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 { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
|
||||
import errorHandler from '../errorHandler';
|
||||
import { DICOMWeb, errorHandler } from '@ohif/core';
|
||||
|
||||
const getImageId = imageObj => {
|
||||
if (!imageObj) {
|
||||
@ -49,7 +47,7 @@ const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => {
|
||||
};
|
||||
|
||||
const cornerstoneRetriever = imageId => {
|
||||
return cornerstone.loadAndCacheImage(imageId).then(image => {
|
||||
return imageLoader.loadAndCacheImage(imageId).then(image => {
|
||||
return image && image.data && image.data.byteArray.buffer;
|
||||
});
|
||||
};
|
||||
@ -151,6 +149,10 @@ class DicomLoaderService {
|
||||
}
|
||||
getDicomDataMethod = fetchIt.bind(this, imageId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported image type: ${loaderType} for imageId: ${imageId}`
|
||||
);
|
||||
}
|
||||
|
||||
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 getPointsFromHandles from './utils/getPointsFromHandles';
|
||||
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
|
||||
const ArrowAnnotate = {
|
||||
toAnnotation: (measurement, definition) => {},
|
||||
const Length = {
|
||||
toAnnotation: measurement => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsAnnotation,
|
||||
csToolsEventDetail,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { element, measurementData } = csToolsAnnotation;
|
||||
const tool =
|
||||
csToolsAnnotation.toolType ||
|
||||
csToolsAnnotation.toolName ||
|
||||
measurementData.toolType;
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(element);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
const points = [];
|
||||
points.push(measurementData.handles);
|
||||
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);
|
||||
|
||||
return {
|
||||
id: measurementData.id,
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: measurementData.text,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
text: measurementData.text,
|
||||
type: getValueTypeFromToolType(tool),
|
||||
points: getPointsFromHandles(measurementData.handles),
|
||||
label: data.text,
|
||||
text: data.text,
|
||||
displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: getValueTypeFromToolType(toolName),
|
||||
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 getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const Bidirectional = {
|
||||
toAnnotation: (measurement, definition) => {},
|
||||
toAnnotation: measurement => {},
|
||||
toMeasurement: (
|
||||
csToolsAnnotation,
|
||||
csToolsEventDetail,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { element, measurementData } = csToolsAnnotation;
|
||||
const tool =
|
||||
csToolsAnnotation.toolType ||
|
||||
csToolsAnnotation.toolName ||
|
||||
measurementData.toolType;
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(element);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
} = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
CornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
const { handles } = measurementData;
|
||||
let displaySet;
|
||||
|
||||
const longAxis = [handles.start, handles.end];
|
||||
const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd];
|
||||
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 {
|
||||
id: measurementData.id,
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: measurementData.label,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
shortestDiameter: measurementData.shortestDiameter,
|
||||
longestDiameter: measurementData.longestDiameter,
|
||||
type: getValueTypeFromToolType(tool),
|
||||
points: { longAxis, shortAxis },
|
||||
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, 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;
|
||||
|
||||
@ -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 getHandlesFromPoints from './utils/getHandlesFromPoints';
|
||||
import getPointsFromHandles from './utils/getPointsFromHandles';
|
||||
import getSOPInstanceAttributes from './utils/getSOPInstanceAttributes';
|
||||
import { DisplaySetService, utils } from '@ohif/core';
|
||||
|
||||
const Length = {
|
||||
toAnnotation: (measurement, definition) => {},
|
||||
toAnnotation: measurement => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
@ -13,49 +12,188 @@ const Length = {
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (
|
||||
csToolsAnnotation,
|
||||
csToolsEventDetail,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
getValueTypeFromToolType
|
||||
) => {
|
||||
const { element, measurementData } = csToolsAnnotation;
|
||||
const tool =
|
||||
csToolsAnnotation.toolType ||
|
||||
csToolsAnnotation.toolName ||
|
||||
measurementData.toolType;
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = getSOPInstanceAttributes(element);
|
||||
|
||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
} = 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 {
|
||||
id: measurementData.id,
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: measurementData.label,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
length: measurementData.length,
|
||||
type: getValueTypeFromToolType(tool),
|
||||
points: getPointsFromHandles(measurementData.handles),
|
||||
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 {
|
||||
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;
|
||||
|
||||
@ -1 +1 @@
|
||||
export default ['Length', 'EllipticalRoi', 'Bidirectional', 'ArrowAnnotate'];
|
||||
export default ['Length', 'EllipticalROI', 'Bidirectional', 'ArrowAnnotate'];
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import Length from './Length';
|
||||
import Bidirectional from './Bidirectional';
|
||||
import EllipticalROI from './EllipticalROI';
|
||||
import ArrowAnnotate from './ArrowAnnotate';
|
||||
import EllipticalRoi from './EllipticalRoi';
|
||||
|
||||
const measurementServiceMappingsFactory = (
|
||||
MeasurementService,
|
||||
DisplaySetService
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService
|
||||
) => {
|
||||
/**
|
||||
* Maps measurement service format object to cornerstone annotation object.
|
||||
@ -19,16 +20,18 @@ const measurementServiceMappingsFactory = (
|
||||
const {
|
||||
POLYLINE,
|
||||
ELLIPSE,
|
||||
POINT,
|
||||
RECTANGLE,
|
||||
BIDIRECTIONAL,
|
||||
POINT,
|
||||
} = 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.
|
||||
// You can't define a bidirectional tool as a single type..
|
||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||
Length: POLYLINE,
|
||||
EllipticalRoi: ELLIPSE,
|
||||
EllipticalROI: ELLIPSE,
|
||||
RectangleROI: RECTANGLE,
|
||||
Bidirectional: BIDIRECTIONAL,
|
||||
ArrowAnnotate: POINT,
|
||||
};
|
||||
@ -43,6 +46,7 @@ const measurementServiceMappingsFactory = (
|
||||
Length.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
matchingCriteria: [
|
||||
@ -58,6 +62,7 @@ const measurementServiceMappingsFactory = (
|
||||
Bidirectional.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
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: {
|
||||
toAnnotation: ArrowAnnotate.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
ArrowAnnotate.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
_getValueTypeFromToolType
|
||||
),
|
||||
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