feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
parent
dc0b183dd4
commit
d57e8bc157
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ yarn-error.log
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
.directory
|
||||||
|
|
||||||
# Common Example Data Directories
|
# Common Example Data Directories
|
||||||
sampledata/
|
sampledata/
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
dc37802ec1f739a6ed602363bdf231d6fe58827e
|
dc37802ec1f739a6ed602363bdf231d6fe58827e
|
||||||
|
|||||||
@ -46,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.68.1",
|
"@cornerstonejs/adapters": "^1.70.0",
|
||||||
"@cornerstonejs/core": "^1.68.1",
|
"@cornerstonejs/core": "^1.70.0",
|
||||||
"@kitware/vtk.js": "29.7.0",
|
"@kitware/vtk.js": "30.3.1",
|
||||||
"react-color": "^2.19.3"
|
"react-color": "^2.19.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import React from 'react';
|
|||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import { Toolbox } from '@ohif/ui';
|
import { Toolbox } from '@ohif/ui';
|
||||||
import PanelSegmentation from './panels/PanelSegmentation';
|
import PanelSegmentation from './panels/PanelSegmentation';
|
||||||
import { SegmentationPanelMode } from './types/segmentation';
|
|
||||||
|
|
||||||
const getPanelModule = ({
|
const getPanelModule = ({
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -17,11 +16,6 @@ const getPanelModule = ({
|
|||||||
const wrappedPanelSegmentation = configuration => {
|
const wrappedPanelSegmentation = configuration => {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const disableEditingForMode = customizationService.get('segmentation.disableEditing');
|
|
||||||
const segmentationPanelMode =
|
|
||||||
customizationService.get('segmentation.segmentationPanelMode')?.value ||
|
|
||||||
SegmentationPanelMode.Dropdown;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSegmentation
|
<PanelSegmentation
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
@ -29,8 +23,8 @@ const getPanelModule = ({
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
disableEditing: appConfig.disableEditing || disableEditingForMode?.value,
|
disableEditing: appConfig.disableEditing,
|
||||||
segmentationPanelMode: segmentationPanelMode,
|
...customizationService.get('segmentation.panel'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -38,9 +32,6 @@ const getPanelModule = ({
|
|||||||
|
|
||||||
const wrappedPanelSegmentationWithTools = configuration => {
|
const wrappedPanelSegmentationWithTools = configuration => {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
const segmentationPanelMode =
|
|
||||||
customizationService.get('segmentation.segmentationPanelMode')?.value ||
|
|
||||||
SegmentationPanelMode.Dropdown;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -60,7 +51,8 @@ const getPanelModule = ({
|
|||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
configuration={{
|
configuration={{
|
||||||
...configuration,
|
...configuration,
|
||||||
segmentationPanelMode: segmentationPanelMode,
|
disableEditing: appConfig.disableEditing,
|
||||||
|
...customizationService.get('segmentation.panel'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -52,16 +52,22 @@ export function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Todo: this is duplicate, we should move it to a shared location
|
||||||
function getToolNameForButton(button) {
|
function getToolNameForButton(button) {
|
||||||
const { props } = button;
|
const { props } = button;
|
||||||
|
|
||||||
const commands = props?.commands || button.commands;
|
const commands = props?.commands || button.commands;
|
||||||
|
const commandsArray = Array.isArray(commands) ? commands : [commands];
|
||||||
if (commands && commands.length) {
|
const firstCommand = commandsArray[0];
|
||||||
const command = commands[0];
|
if (typeof firstCommand === 'string') {
|
||||||
const { commandOptions } = command;
|
// likely not a cornerstone tool
|
||||||
const { toolName } = commandOptions || { toolName: props?.id ?? button.id };
|
return null;
|
||||||
return toolName;
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
if ('commandOptions' in firstCommand) {
|
||||||
|
return firstCommand.commandOptions.toolName ?? props?.id ?? button.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use id as a fallback for toolName
|
||||||
|
return props?.id ?? button.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import getHangingProtocolModule from './getHangingProtocolModule';
|
|||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import getCommandsModule from './commandsModule';
|
import getCommandsModule from './commandsModule';
|
||||||
import { getToolbarModule } from './getToolbarModule';
|
import { getToolbarModule } from './getToolbarModule';
|
||||||
import preRegistration from './init';
|
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
||||||
@ -29,7 +28,6 @@ const extension = {
|
|||||||
* You ID can be anything you want, but it should be unique.
|
* You ID can be anything you want, but it should be unique.
|
||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
preRegistration,
|
|
||||||
/**
|
/**
|
||||||
* PanelModule should provide a list of panels that will be available in OHIF
|
* PanelModule should provide a list of panels that will be available in OHIF
|
||||||
* for Modes to consume and render. Each panel is defined by a {name,
|
* for Modes to consume and render. Each panel is defined by a {name,
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
import { addTool, BrushTool } from '@cornerstonejs/tools';
|
|
||||||
|
|
||||||
export default function init({ servicesManager }): void {
|
|
||||||
addTool(BrushTool);
|
|
||||||
}
|
|
||||||
@ -242,7 +242,13 @@ export default function PanelSegmentation({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const SegmentationGroupTableComponent = components[configuration?.segmentationPanelMode];
|
const SegmentationGroupTableComponent =
|
||||||
|
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
|
||||||
|
const allowAddSegment = configuration?.addSegment;
|
||||||
|
const onSegmentationAddWrapper =
|
||||||
|
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
|
||||||
|
? configuration?.onSegmentationAdd
|
||||||
|
: onSegmentationAdd;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SegmentationGroupTableComponent
|
<SegmentationGroupTableComponent
|
||||||
@ -250,7 +256,8 @@ export default function PanelSegmentation({
|
|||||||
segmentations={segmentations}
|
segmentations={segmentations}
|
||||||
disableEditing={configuration.disableEditing}
|
disableEditing={configuration.disableEditing}
|
||||||
activeSegmentationId={selectedSegmentationId || ''}
|
activeSegmentationId={selectedSegmentationId || ''}
|
||||||
onSegmentationAdd={onSegmentationAdd}
|
onSegmentationAdd={onSegmentationAddWrapper}
|
||||||
|
showAddSegment={allowAddSegment}
|
||||||
onSegmentationClick={onSegmentationClick}
|
onSegmentationClick={onSegmentationClick}
|
||||||
onSegmentationDelete={onSegmentationDelete}
|
onSegmentationDelete={onSegmentationDelete}
|
||||||
onSegmentationDownload={onSegmentationDownload}
|
onSegmentationDownload={onSegmentationDownload}
|
||||||
|
|||||||
@ -46,9 +46,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.68.1",
|
"@cornerstonejs/adapters": "^1.70.0",
|
||||||
"@cornerstonejs/core": "^1.68.1",
|
"@cornerstonejs/core": "^1.70.0",
|
||||||
"@cornerstonejs/tools": "^1.68.1",
|
"@cornerstonejs/tools": "^1.70.0",
|
||||||
"classnames": "^2.3.2"
|
"classnames": "^2.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -378,10 +378,13 @@ async function _getViewportReferencedDisplaySetData(
|
|||||||
measurementSelected,
|
measurementSelected,
|
||||||
displaySetService
|
displaySetService
|
||||||
) {
|
) {
|
||||||
const { measurements } = displaySet;
|
if (!displaySet.keyImageDisplaySet) {
|
||||||
const measurement = measurements[measurementSelected];
|
// Create a new display set, and preserve a reference to it here,
|
||||||
|
// so that it can be re-displayed and shown inside the SR viewport.
|
||||||
const { displaySetInstanceUID } = measurement;
|
// This is only for ease of redisplay - the display set is stored in the
|
||||||
|
// usual manner in the display set service.
|
||||||
|
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
};
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
|
||||||
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.join(__dirname, '../');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
const ENTRY = {
|
||||||
|
app: `${SRC_DIR}/index.ts`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||||
|
|
||||||
|
return merge(commonConfig, {
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: ROOT_DIR,
|
||||||
|
library: 'ohif-extension-cornerstone',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
filename: pkg.main,
|
||||||
|
},
|
||||||
|
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: `./dist/${outputName}.css`,
|
||||||
|
chunkFilename: `./dist/${outputName}.css`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
20
extensions/cornerstone-dynamic-volume/LICENSE
Normal file
20
extensions/cornerstone-dynamic-volume/LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 cornerstone-dynamic-volume ()
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
8
extensions/cornerstone-dynamic-volume/README.md
Normal file
8
extensions/cornerstone-dynamic-volume/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# cornerstone-dynamic-volume
|
||||||
|
## Description
|
||||||
|
|
||||||
|
## Author
|
||||||
|
OHIF
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
1
extensions/cornerstone-dynamic-volume/babel.config.js
Normal file
1
extensions/cornerstone-dynamic-volume/babel.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('../../babel.config.js');
|
||||||
50
extensions/cornerstone-dynamic-volume/package.json
Normal file
50
extensions/cornerstone-dynamic-volume/package.json
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||||
|
"version": "3.8.0-beta.72",
|
||||||
|
"description": "OHIF extension for 4D volumes data",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
|
||||||
|
"module": "src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./types": "./src/types/index.ts"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package": "yarn run build",
|
||||||
|
"clean": "shx rm -rf dist",
|
||||||
|
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||||
|
"start": "yarn run dev",
|
||||||
|
"test:unit": "jest --watchAll",
|
||||||
|
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "3.7.0-beta.76",
|
||||||
|
"@ohif/ui": "3.7.0-beta.76",
|
||||||
|
"@ohif/extension-default": "3.7.0-beta.76",
|
||||||
|
"@ohif/extension-cornerstone": "3.7.0-beta.76",
|
||||||
|
"@ohif/i18n": "3.7.0-beta.76",
|
||||||
|
"dcmjs": "^0.29.5",
|
||||||
|
"dicom-parser": "^1.8.21",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^17.0.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@cornerstonejs/tools": "^1.70.0",
|
||||||
|
"@cornerstonejs/core": "^1.70.0",
|
||||||
|
"@cornerstonejs/streaming-image-volume-loader": "^1.70.0",
|
||||||
|
"classnames": "^2.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
|
||||||
|
|
||||||
|
export { updateSegmentationsChartDisplaySet };
|
||||||
@ -0,0 +1,281 @@
|
|||||||
|
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import * as cs from '@cornerstonejs/core';
|
||||||
|
import * as csTools from '@cornerstonejs/tools';
|
||||||
|
|
||||||
|
const CHART_MODALITY = 'CHT';
|
||||||
|
const SEG_CHART_INSTANCE_UID = utils.guid();
|
||||||
|
|
||||||
|
// Private SOPClassUid for chart data
|
||||||
|
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
|
||||||
|
|
||||||
|
const { utilities: csToolsUtils } = csTools;
|
||||||
|
|
||||||
|
function _getDateTimeStr() {
|
||||||
|
const now = new Date();
|
||||||
|
const date =
|
||||||
|
now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2);
|
||||||
|
const time =
|
||||||
|
('0' + now.getUTCHours()).slice(-2) +
|
||||||
|
('0' + now.getUTCMinutes()).slice(-2) +
|
||||||
|
('0' + now.getUTCSeconds()).slice(-2);
|
||||||
|
|
||||||
|
return { date, time };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getTimePointsDataByTagName(volume, timePointsTag) {
|
||||||
|
const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => {
|
||||||
|
const instance = DicomMetadataStore.getInstanceByImageId(imageId);
|
||||||
|
const timePointValue = instance[timePointsTag];
|
||||||
|
|
||||||
|
if (timePointValue !== undefined) {
|
||||||
|
timePoints.add(timePointValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return timePoints;
|
||||||
|
}, new Set());
|
||||||
|
|
||||||
|
return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _convertTimePointsUnit(timePoints, timePointsUnit) {
|
||||||
|
const validUnits = ['ms', 's', 'm', 'h'];
|
||||||
|
const divisors = [1000, 60, 60];
|
||||||
|
const currentUnitIndex = validUnits.indexOf(timePointsUnit);
|
||||||
|
let divisor = 1;
|
||||||
|
|
||||||
|
if (currentUnitIndex !== -1) {
|
||||||
|
for (let i = currentUnitIndex; i < validUnits.length - 1; i++) {
|
||||||
|
const newDivisor = divisor * divisors[i];
|
||||||
|
const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length;
|
||||||
|
|
||||||
|
// Change the scale only if more than 50% of the time points are
|
||||||
|
// greater than the new divisor.
|
||||||
|
if (greaterThanDivisorCount <= timePoints.length / 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
divisor = newDivisor;
|
||||||
|
timePointsUnit = validUnits[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (divisor > 1) {
|
||||||
|
timePoints = timePoints.map(timePoint => timePoint / divisor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { timePoints, timePointsUnit };
|
||||||
|
}
|
||||||
|
|
||||||
|
// It currently supports only one tag but a few other will be added soon
|
||||||
|
// Supported 4D Tags
|
||||||
|
// (0018,1060) Trigger Time [NOK]
|
||||||
|
// (0018,0081) Echo Time [NOK]
|
||||||
|
// (0018,0086) Echo Number [NOK]
|
||||||
|
// (0020,0100) Temporal Position Identifier [NOK]
|
||||||
|
// (0054,1300) FrameReferenceTime [OK]
|
||||||
|
function _getTimePointsData(volume) {
|
||||||
|
const timePointsTags = {
|
||||||
|
FrameReferenceTime: {
|
||||||
|
unit: 'ms',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const timePointsTagNames = Object.keys(timePointsTags);
|
||||||
|
let timePoints;
|
||||||
|
let timePointsUnit;
|
||||||
|
|
||||||
|
for (let i = 0; i < timePointsTagNames.length; i++) {
|
||||||
|
const tagName = timePointsTagNames[i];
|
||||||
|
const curTimePoints = _getTimePointsDataByTagName(volume, tagName);
|
||||||
|
|
||||||
|
if (curTimePoints.length) {
|
||||||
|
timePoints = curTimePoints;
|
||||||
|
timePointsUnit = timePointsTags[tagName].unit;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timePoints.length) {
|
||||||
|
const concatTagNames = timePointsTagNames.join(', ');
|
||||||
|
|
||||||
|
throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit);
|
||||||
|
|
||||||
|
timePoints = convertedTimePoints.timePoints;
|
||||||
|
timePointsUnit = convertedTimePoints.timePointsUnit;
|
||||||
|
|
||||||
|
return { timePoints, timePointsUnit };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
|
||||||
|
const displaySets = displaySetService.getActiveDisplaySets();
|
||||||
|
|
||||||
|
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||||
|
const anInstance = displaySet.instances?.[0];
|
||||||
|
|
||||||
|
if (anInstance) {
|
||||||
|
return (
|
||||||
|
anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID);
|
||||||
|
let volumeCacheKey: string | undefined;
|
||||||
|
const volumeId = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
|
for (const [key] of cs.cache._volumeCache) {
|
||||||
|
if (key.includes(volumeId)) {
|
||||||
|
volumeCacheKey = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let referencedDynamicVolume;
|
||||||
|
if (volumeCacheKey) {
|
||||||
|
referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId(
|
||||||
|
referencedDynamicVolume.imageIds[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||||
|
maskVolumeId: segmentation.id,
|
||||||
|
}) as number[][];
|
||||||
|
|
||||||
|
const pixelCount = timeData.length;
|
||||||
|
|
||||||
|
if (pixelCount === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// since we only use one segmentation representation per segmentationId
|
||||||
|
// it is fine to pick the first one
|
||||||
|
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
|
||||||
|
segmentation.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const segmentationRepresentationUID =
|
||||||
|
segmentationRepresentations[0].segmentationRepresentationUID;
|
||||||
|
|
||||||
|
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
|
||||||
|
segmentationRepresentationUID
|
||||||
|
);
|
||||||
|
|
||||||
|
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||||
|
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
||||||
|
toolGroupId,
|
||||||
|
segmentationRepresentationUID,
|
||||||
|
1 // segmentIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const hexColor = cs.utilities.color.rgbToHex(...color);
|
||||||
|
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
||||||
|
|
||||||
|
if (!timePointsData) {
|
||||||
|
timePointsData = _getTimePointsData(referencedDynamicVolume);
|
||||||
|
volumesTimePointsCache.set(referencedDynamicVolume, timePointsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { timePoints, timePointsUnit } = timePointsData;
|
||||||
|
|
||||||
|
if (timePoints.length !== timeData[0].length) {
|
||||||
|
throw new Error('Invalid number of time points returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
const timepointsCount = timePoints.length;
|
||||||
|
const chartSeriesData = new Array(timepointsCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < timepointsCount; i++) {
|
||||||
|
const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0);
|
||||||
|
|
||||||
|
chartSeriesData[i] = [timePoints[i], average];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
StudyInstanceUID,
|
||||||
|
StudyDescription,
|
||||||
|
chartData: {
|
||||||
|
series: {
|
||||||
|
label: segmentation.label,
|
||||||
|
points: chartSeriesData,
|
||||||
|
color: hexColor,
|
||||||
|
},
|
||||||
|
axis: {
|
||||||
|
x: {
|
||||||
|
label: `Time (${timePointsUnit})`,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
label: `Vl (Bq/ml)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||||
|
if (!segmentations.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumesTimePointsCache = new WeakMap();
|
||||||
|
const segmentationsData = segmentations.map(segmentation =>
|
||||||
|
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
||||||
|
);
|
||||||
|
|
||||||
|
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||||
|
const series = segmentationsData.reduce((allSeries, curSegData) => {
|
||||||
|
return [...allSeries, curSegData.chartData.series];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const instance = {
|
||||||
|
SOPClassUID: ChartDataSOPClassUid,
|
||||||
|
Modality: CHART_MODALITY,
|
||||||
|
SOPInstanceUID: utils.guid(),
|
||||||
|
SeriesDate: seriesDate,
|
||||||
|
SeriesTime: seriesTime,
|
||||||
|
SeriesInstanceUID: SEG_CHART_INSTANCE_UID,
|
||||||
|
StudyInstanceUID: segmentationsData[0].StudyInstanceUID,
|
||||||
|
StudyDescription: segmentationsData[0].StudyDescription,
|
||||||
|
SeriesNumber: 100,
|
||||||
|
SeriesDescription: 'Segmentation chart series data',
|
||||||
|
chartData: {
|
||||||
|
series,
|
||||||
|
axis: { ...segmentationsData[0].chartData.axis },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const seriesMetadata = {
|
||||||
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
|
StudyDescription: instance.StudyDescription,
|
||||||
|
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
|
SeriesDescription: instance.SeriesDescription,
|
||||||
|
SeriesNumber: instance.SeriesNumber,
|
||||||
|
SeriesTime: instance.SeriesTime,
|
||||||
|
SOPClassUID: instance.SOPClassUID,
|
||||||
|
Modality: instance.Modality,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { seriesMetadata, instance };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSegmentationsChartDisplaySet({ servicesManager }): void {
|
||||||
|
const { segmentationService, displaySetService } = servicesManager.services;
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
const { seriesMetadata, instance } =
|
||||||
|
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
||||||
|
|
||||||
|
if (seriesMetadata && instance) {
|
||||||
|
// An event is triggered after adding the instance and the displaySet is created
|
||||||
|
DicomMetadataStore.addSeriesMetadata([seriesMetadata], true);
|
||||||
|
DicomMetadataStore.addInstances([instance], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { updateSegmentationsChartDisplaySet as default };
|
||||||
407
extensions/cornerstone-dynamic-volume/src/commandsModule.ts
Normal file
407
extensions/cornerstone-dynamic-volume/src/commandsModule.ts
Normal file
@ -0,0 +1,407 @@
|
|||||||
|
import * as importedActions from './actions';
|
||||||
|
import { utilities, Enums } from '@cornerstonejs/tools';
|
||||||
|
import { cache } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
|
||||||
|
|
||||||
|
const commandsModule = ({ commandsManager, servicesManager }) => {
|
||||||
|
const services = servicesManager.services;
|
||||||
|
const { displaySetService, viewportGridService, segmentationService } = services;
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
...importedActions,
|
||||||
|
getDynamic4DDisplaySet: () => {
|
||||||
|
const displaySets = displaySetService.getActiveDisplaySets();
|
||||||
|
|
||||||
|
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||||
|
const anInstance = displaySet.instances?.[0];
|
||||||
|
|
||||||
|
if (anInstance) {
|
||||||
|
return (
|
||||||
|
anInstance.FrameReferenceTime !== undefined ||
|
||||||
|
anInstance.NumberOfTimeSlices !== undefined ||
|
||||||
|
anInstance.TemporalPositionIdentifier !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return dynamic4DDisplaySet;
|
||||||
|
},
|
||||||
|
getComputedDisplaySets: () => {
|
||||||
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||||
|
const cachedDisplaySets = [...displaySetCache.values()];
|
||||||
|
const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
|
||||||
|
return displaySet.isDerived;
|
||||||
|
});
|
||||||
|
return computedDisplaySets;
|
||||||
|
},
|
||||||
|
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
|
||||||
|
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||||
|
|
||||||
|
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
|
||||||
|
|
||||||
|
// cache._volumeCache is a map that has a key that includes the volumeId
|
||||||
|
// it is not exactly the volumeId, but it is the key that includes the volumeId
|
||||||
|
// so we can't do cache._volumeCache.get(volumeId) we should iterate
|
||||||
|
// over the keys and find the one that includes the volumeId
|
||||||
|
let volumeCacheKey: string | undefined;
|
||||||
|
|
||||||
|
for (const [key] of cache._volumeCache) {
|
||||||
|
if (key.includes(volumeId)) {
|
||||||
|
volumeCacheKey = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dynamicVolume;
|
||||||
|
if (volumeCacheKey) {
|
||||||
|
dynamicVolume = cache.getVolume(volumeCacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = dynamic4DDisplaySet.instances[0];
|
||||||
|
|
||||||
|
const csv = [];
|
||||||
|
|
||||||
|
// CSV header information with placeholder empty values for the metadata lines
|
||||||
|
csv.push(`Patient ID,${instance.PatientID},`);
|
||||||
|
csv.push(`Study Date,${instance.StudyDate},`);
|
||||||
|
csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
|
||||||
|
csv.push(`StudyDescription,${instance.StudyDescription},`);
|
||||||
|
csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);
|
||||||
|
|
||||||
|
// empty line
|
||||||
|
csv.push('');
|
||||||
|
csv.push('');
|
||||||
|
|
||||||
|
// Helper function to calculate standard deviation
|
||||||
|
function calculateStandardDeviation(data) {
|
||||||
|
const n = data.length;
|
||||||
|
const mean = data.reduce((acc, value) => acc + value, 0) / n;
|
||||||
|
const squaredDifferences = data.map(value => (value - mean) ** 2);
|
||||||
|
const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
|
||||||
|
const stdDeviation = Math.sqrt(variance);
|
||||||
|
return stdDeviation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through each segmentation to get the timeData and ijkCoords
|
||||||
|
segmentations.forEach((segmentation, segmentationIndex) => {
|
||||||
|
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||||
|
maskVolumeId: segmentation.id,
|
||||||
|
}) as number[][];
|
||||||
|
|
||||||
|
if (summaryStats) {
|
||||||
|
// Adding column headers for pixel identifier and segmentation label ids
|
||||||
|
let headers = 'Operation,Segmentation Label ID';
|
||||||
|
const maxLength = dynamicVolume.numTimePoints;
|
||||||
|
for (let t = 0; t < maxLength; t++) {
|
||||||
|
headers += `,Time Point ${t}`;
|
||||||
|
}
|
||||||
|
csv.push(headers);
|
||||||
|
// // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
|
||||||
|
// // all the voxels in the ROI
|
||||||
|
const mean = [];
|
||||||
|
const min = [];
|
||||||
|
const minIJK = [];
|
||||||
|
const max = [];
|
||||||
|
const maxIJK = [];
|
||||||
|
const std = [];
|
||||||
|
|
||||||
|
const numVoxels = timeData.length;
|
||||||
|
// Helper function to calculate standard deviation
|
||||||
|
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||||
|
// for each voxel in the ROI, get the value at the current time point
|
||||||
|
const voxelValues = [];
|
||||||
|
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||||
|
voxelValues.push(timeData[voxelIndex][timeIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
|
||||||
|
const minimum = Math.min(...voxelValues);
|
||||||
|
min.push(minimum);
|
||||||
|
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
||||||
|
const maximum = Math.max(...voxelValues);
|
||||||
|
max.push(maximum);
|
||||||
|
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
||||||
|
std.push(calculateStandardDeviation(voxelValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = `Mean,${segmentation.label}`;
|
||||||
|
// Generate separate rows for each statistic
|
||||||
|
for (let t = 0; t < maxLength; t++) {
|
||||||
|
row += `,${mean[t]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
csv.push(row);
|
||||||
|
|
||||||
|
row = `Standard Deviation,${segmentation.label}`;
|
||||||
|
for (let t = 0; t < maxLength; t++) {
|
||||||
|
row += `,${std[t]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
csv.push(row);
|
||||||
|
|
||||||
|
row = `Min,${segmentation.label}`;
|
||||||
|
for (let t = 0; t < maxLength; t++) {
|
||||||
|
row += `,${min[t]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
csv.push(row);
|
||||||
|
|
||||||
|
row = `Max,${segmentation.label}`;
|
||||||
|
for (let t = 0; t < maxLength; t++) {
|
||||||
|
row += `,${max[t]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
csv.push(row);
|
||||||
|
} else {
|
||||||
|
// Adding column headers for pixel identifier and segmentation label ids
|
||||||
|
let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
|
||||||
|
const maxLength = dynamicVolume.numTimePoints;
|
||||||
|
for (let t = 0; t < maxLength; t++) {
|
||||||
|
headers += `,Time Point ${t}`;
|
||||||
|
}
|
||||||
|
csv.push(headers);
|
||||||
|
// Assuming timeData and ijkCoords are of the same length
|
||||||
|
for (let i = 0; i < timeData.length; i++) {
|
||||||
|
// Generate the pixel identifier
|
||||||
|
const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;
|
||||||
|
|
||||||
|
// Start a new row for the current pixel
|
||||||
|
let row = `${pixelIdentifier},${segmentation.label}`;
|
||||||
|
|
||||||
|
// Add time data points for this pixel
|
||||||
|
for (let t = 0; t < timeData[i].length; t++) {
|
||||||
|
row += `,${timeData[i][t]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the row to the CSV array
|
||||||
|
csv.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to CSV string
|
||||||
|
const csvContent = csv.join('\n');
|
||||||
|
|
||||||
|
// Generate filename and trigger download
|
||||||
|
const filename = `${instance.PatientID}.csv`;
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', filename);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
},
|
||||||
|
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
|
||||||
|
const computedDisplaySet = displaySet;
|
||||||
|
|
||||||
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||||
|
const cachedDisplaySetKeys = [displaySetCache.keys()];
|
||||||
|
const { displaySetInstanceUID } = computedDisplaySet;
|
||||||
|
// Check to see if computed display set is already in cache
|
||||||
|
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||||
|
displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all viewports and their corresponding indices
|
||||||
|
const { viewports } = viewportGridService.getState();
|
||||||
|
|
||||||
|
// get the viewports in the grid
|
||||||
|
// iterate over them and find the ones that are showing a dynamic
|
||||||
|
// volume (displaySet), and replace that exact displaySet with the
|
||||||
|
// computed displaySet
|
||||||
|
|
||||||
|
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||||
|
|
||||||
|
const viewportsToUpdate = [];
|
||||||
|
|
||||||
|
for (const [key, value] of viewports) {
|
||||||
|
const viewport = value;
|
||||||
|
const viewportOptions = viewport.viewportOptions;
|
||||||
|
const { displaySetInstanceUIDs } = viewport;
|
||||||
|
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||||
|
dynamic4DDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
if (displaySetInstanceUIDIndex !== -1) {
|
||||||
|
const newViewport = {
|
||||||
|
viewportId: viewport.viewportId,
|
||||||
|
// merge the other displaySetInstanceUIDs with the new one
|
||||||
|
displaySetInstanceUIDs: [
|
||||||
|
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||||
|
displaySetInstanceUID,
|
||||||
|
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||||
|
],
|
||||||
|
viewportOptions: {
|
||||||
|
initialImageOptions: viewportOptions.initialImageOptions,
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: viewportOptions.orientation,
|
||||||
|
background: viewportOptions.background,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewportsToUpdate.push(newViewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||||
|
},
|
||||||
|
swapComputedWithDynamicDisplaySet: () => {
|
||||||
|
// Todo: this assumes there is only one dynamic display set in the viewer
|
||||||
|
const dynamicDisplaySet = actions.getDynamic4DDisplaySet();
|
||||||
|
|
||||||
|
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||||
|
const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
|
||||||
|
const { displaySetInstanceUID } = dynamicDisplaySet;
|
||||||
|
|
||||||
|
// Check to see if dynamic display set is already in cache
|
||||||
|
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||||
|
displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all viewports and their corresponding indices
|
||||||
|
const { viewports } = viewportGridService.getState();
|
||||||
|
|
||||||
|
// Get the computed 4D display set
|
||||||
|
const computed4DDisplaySet = actions.getComputedDisplaySets()[0];
|
||||||
|
|
||||||
|
const viewportsToUpdate = [];
|
||||||
|
|
||||||
|
for (const [key, value] of viewports) {
|
||||||
|
const viewport = value;
|
||||||
|
const viewportOptions = viewport.viewportOptions;
|
||||||
|
const { displaySetInstanceUIDs } = viewport;
|
||||||
|
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||||
|
computed4DDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
if (displaySetInstanceUIDIndex !== -1) {
|
||||||
|
const newViewport = {
|
||||||
|
viewportId: viewport.viewportId,
|
||||||
|
// merge the other displaySetInstanceUIDs with the new one
|
||||||
|
displaySetInstanceUIDs: [
|
||||||
|
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||||
|
displaySetInstanceUID,
|
||||||
|
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||||
|
],
|
||||||
|
viewportOptions: {
|
||||||
|
initialImageOptions: viewportOptions.initialImageOptions,
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: viewportOptions.orientation,
|
||||||
|
background: viewportOptions.background,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
viewportsToUpdate.push(newViewport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||||
|
},
|
||||||
|
createNewLabelMapForDynamicVolume: async ({ label }) => {
|
||||||
|
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||||
|
|
||||||
|
// get the dynamic 4D display set
|
||||||
|
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||||
|
const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||||
|
|
||||||
|
// check if the dynamic 4D display set is in the display, if not we might have
|
||||||
|
// the computed volumes and we should choose them for the segmentation
|
||||||
|
// creation
|
||||||
|
|
||||||
|
let referenceDisplaySet;
|
||||||
|
|
||||||
|
const activeViewport = viewports.get(activeViewportId);
|
||||||
|
const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
|
||||||
|
const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
|
||||||
|
dynamic4DDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dynamicIsInActiveViewport) {
|
||||||
|
referenceDisplaySet = dynamic4DDisplaySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!referenceDisplaySet) {
|
||||||
|
// try to see if there is any derived displaySet in the active viewport
|
||||||
|
// which is referencing the dynamic 4D display set
|
||||||
|
|
||||||
|
// Todo: this is wrong but I don't have time to fix it now
|
||||||
|
const cachedDisplaySets = displaySetService.getDisplaySetCache();
|
||||||
|
for (const [key, displaySet] of cachedDisplaySets) {
|
||||||
|
if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
|
||||||
|
referenceDisplaySet = displaySet;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!referenceDisplaySet) {
|
||||||
|
throw new Error('No reference display set found based on the dynamic data');
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||||
|
referenceDisplaySet.displaySetInstanceUID,
|
||||||
|
{ label }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add Segmentation to all toolGroupIds in the viewer
|
||||||
|
const toolGroupIds = Array.from(
|
||||||
|
viewports.values(),
|
||||||
|
viewport => viewport.viewportOptions.toolGroupId
|
||||||
|
);
|
||||||
|
|
||||||
|
const representationType = LABELMAP;
|
||||||
|
|
||||||
|
for (const toolGroupId of toolGroupIds) {
|
||||||
|
const hydrateSegmentation = true;
|
||||||
|
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||||
|
toolGroupId,
|
||||||
|
segmentationId,
|
||||||
|
hydrateSegmentation,
|
||||||
|
representationType
|
||||||
|
);
|
||||||
|
|
||||||
|
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return segmentationId;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const definitions = {
|
||||||
|
updateSegmentationsChartDisplaySet: {
|
||||||
|
commandFn: actions.updateSegmentationsChartDisplaySet,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
exportTimeReportCSV: {
|
||||||
|
commandFn: actions.exportTimeReportCSV,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
swapDynamicWithComputedDisplaySet: {
|
||||||
|
commandFn: actions.swapDynamicWithComputedDisplaySet,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
createNewLabelMapForDynamicVolume: {
|
||||||
|
commandFn: actions.createNewLabelMapForDynamicVolume,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
swapComputedWithDynamicDisplaySet: {
|
||||||
|
commandFn: actions.swapComputedWithDynamicDisplaySet,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
actions,
|
||||||
|
definitions,
|
||||||
|
defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default commandsModule;
|
||||||
@ -0,0 +1,654 @@
|
|||||||
|
const DEFAULT_COLORMAP = '2hot';
|
||||||
|
const toolGroupIds = {
|
||||||
|
pt: 'dynamic4D-pt',
|
||||||
|
fusion: 'dynamic4D-fusion',
|
||||||
|
ct: 'dynamic4D-ct',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPTOptions({
|
||||||
|
colormap,
|
||||||
|
voiInverted,
|
||||||
|
}: {
|
||||||
|
colormap?: {
|
||||||
|
name: string;
|
||||||
|
opacity:
|
||||||
|
| number
|
||||||
|
| {
|
||||||
|
value: number;
|
||||||
|
opacity: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
voiInverted?: boolean;
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
blendMode: 'MIP',
|
||||||
|
colormap,
|
||||||
|
voi: {
|
||||||
|
windowWidth: 5,
|
||||||
|
windowCenter: 2.5,
|
||||||
|
},
|
||||||
|
voiInverted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPTViewports() {
|
||||||
|
const ptOptionsParams = {
|
||||||
|
colormap: {
|
||||||
|
name: DEFAULT_COLORMAP,
|
||||||
|
opacity: [
|
||||||
|
{ value: 0, opacity: 0 },
|
||||||
|
{ value: 0.1, opacity: 1 },
|
||||||
|
{ value: 1, opacity: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
voiInverted: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ptAxial',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: toolGroupIds.pt,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
options: { ...getPTOptions(ptOptionsParams) },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ptSagittal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
toolGroupId: toolGroupIds.pt,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'sagittalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
options: { ...getPTOptions(ptOptionsParams) },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ptCoronal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
toolGroupId: toolGroupIds.pt,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'coronalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
options: { ...getPTOptions(ptOptionsParams) },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFusionViewports() {
|
||||||
|
const ptOptionsParams = {
|
||||||
|
colormap: {
|
||||||
|
name: DEFAULT_COLORMAP,
|
||||||
|
opacity: [
|
||||||
|
{ value: 0, opacity: 0 },
|
||||||
|
{ value: 0.1, opacity: 0.3 },
|
||||||
|
{ value: 1, opacity: 0.3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'fusionAxial',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: toolGroupIds.fusion,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'fusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncInvertState: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: { ...getPTOptions(ptOptionsParams) },
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'fusionSagittal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
toolGroupId: toolGroupIds.fusion,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'sagittalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'fusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncInvertState: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: { ...getPTOptions(ptOptionsParams) },
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'fusionCoronal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
toolGroupId: toolGroupIds.fusion,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'coronalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'fusionWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ptFusionWLSync',
|
||||||
|
source: false,
|
||||||
|
target: true,
|
||||||
|
options: {
|
||||||
|
syncInvertState: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
options: { ...getPTOptions(ptOptionsParams) },
|
||||||
|
id: 'ptDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeriesChartViewport() {
|
||||||
|
return {
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'seriesChart',
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'chartDisplaySet',
|
||||||
|
options: {
|
||||||
|
// This dataset does not require the download of any instance since it is pre-computed locally,
|
||||||
|
// but interleaveTopToBottom.ts was not loading any series because it consider that all viewports
|
||||||
|
// are a Cornerstone viewport which is not true in this case and it waits for all viewports to
|
||||||
|
// have called interleaveTopToBottom(...).
|
||||||
|
skipLoading: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCTViewports() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctAxial',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
toolGroupId: toolGroupIds.ct,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle', // 'first', 'last', 'middle'
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'axialSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctSagittal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'sagittal',
|
||||||
|
toolGroupId: toolGroupIds.ct,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'sagittalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
viewportId: 'ctCoronal',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'coronal',
|
||||||
|
toolGroupId: toolGroupIds.ct,
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'cameraPosition',
|
||||||
|
id: 'coronalSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'ctWLSync',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'ctDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProtocol = {
|
||||||
|
id: 'default4D',
|
||||||
|
locked: true,
|
||||||
|
// Don't store this hanging protocol as it applies to the currently active
|
||||||
|
// display set by default
|
||||||
|
// cacheId: null,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'Default',
|
||||||
|
createdDate: '2023-01-01T00:00:00.000Z',
|
||||||
|
modifiedDate: '2023-01-01T00:00:00.000Z',
|
||||||
|
availableTo: {},
|
||||||
|
editableBy: {},
|
||||||
|
imageLoadStrategy: 'default', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||||
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'ModalitiesInStudy',
|
||||||
|
constraint: {
|
||||||
|
contains: ['CT', 'PT'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// -1 would be used to indicate active only, whereas other values are
|
||||||
|
// the number of required priors referenced - so 0 means active with
|
||||||
|
// 0 or more priors.
|
||||||
|
numberOfPriorsReferenced: -1,
|
||||||
|
displaySetSelectors: {
|
||||||
|
defaultDisplaySetId: {
|
||||||
|
// Unused currently
|
||||||
|
imageMatchingRules: [],
|
||||||
|
// Matches displaysets, NOT series
|
||||||
|
seriesMatchingRules: [
|
||||||
|
// Try to match series with images by default, to prevent weird display
|
||||||
|
// on SEG/SR containing studies
|
||||||
|
{
|
||||||
|
attribute: 'numImageFrames',
|
||||||
|
constraint: {
|
||||||
|
greaterThan: { value: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Can be used to select matching studies
|
||||||
|
// studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
ctDisplaySet: {
|
||||||
|
// Unused currently
|
||||||
|
imageMatchingRules: [],
|
||||||
|
// Matches displaysets, NOT series
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: 'CT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute: 'isReconstructable',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Can be used to select matching studies
|
||||||
|
// studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
ptDisplaySet: {
|
||||||
|
// Unused currently
|
||||||
|
imageMatchingRules: [],
|
||||||
|
// Matches displaysets, NOT series
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: 'PT',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute: 'isReconstructable',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
attribute: 'SeriesDescription',
|
||||||
|
constraint: {
|
||||||
|
contains: 'Corrected',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
weight: 2,
|
||||||
|
attribute: 'SeriesDescription',
|
||||||
|
constraint: {
|
||||||
|
doesNotContain: {
|
||||||
|
value: 'Uncorrected',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Should we check if CorrectedImage contains ATTN?
|
||||||
|
// (0028,0051) (CorrectedImage): NORM\DTIM\ATTN\SCAT\RADL\DECY
|
||||||
|
],
|
||||||
|
// Can be used to select matching studies
|
||||||
|
// studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
chartDisplaySet: {
|
||||||
|
// Unused currently
|
||||||
|
imageMatchingRules: [],
|
||||||
|
// Matches displaysets, NOT series
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
attribute: 'Modality',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: 'CHT',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: 'dataPreparation',
|
||||||
|
name: 'Data Preparation',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [...getPTViewports()],
|
||||||
|
createdDate: '2023-01-01T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'registration',
|
||||||
|
name: 'Registration',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 3,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [...getFusionViewports(), ...getCTViewports(), ...getPTViewports()],
|
||||||
|
createdDate: '2023-01-01T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'roiQuantification',
|
||||||
|
name: 'ROI Quantification',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [...getFusionViewports()],
|
||||||
|
createdDate: '2023-01-01T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'kineticAnalysis',
|
||||||
|
name: 'Kinetic Analysis',
|
||||||
|
viewportStructure: {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 3,
|
||||||
|
layoutOptions: [
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 3,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 1 / 3,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 3,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 2 / 3,
|
||||||
|
y: 0,
|
||||||
|
width: 1 / 3,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 1 / 2,
|
||||||
|
width: 1,
|
||||||
|
height: 1 / 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [...getFusionViewports(), getSeriesChartViewport()],
|
||||||
|
createdDate: '2023-01-01T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HangingProtocolModule should provide a list of hanging protocols that will be
|
||||||
|
* available in OHIF for Modes to use to decide on the structure of the viewports
|
||||||
|
* and also the series that hung in the viewports. Each hanging protocol is defined by
|
||||||
|
* { name, protocols}. Examples include the default hanging protocol provided by
|
||||||
|
* the default extension that shows 2x2 viewports.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getHangingProtocolModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: defaultProtocol.id,
|
||||||
|
protocol: defaultProtocol,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getHangingProtocolModule;
|
||||||
68
extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx
Normal file
68
extensions/cornerstone-dynamic-volume/src/getPanelModule.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DynamicDataPanel } from './panels';
|
||||||
|
import { Toolbox } from '@ohif/ui';
|
||||||
|
import DynamicExport from './panels/DynamicExport';
|
||||||
|
|
||||||
|
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||||
|
const wrappedDynamicDataPanel = () => {
|
||||||
|
return (
|
||||||
|
<DynamicDataPanel
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedDynamicToolbox = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Toolbox
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
buttonSectionId="dynamic-toolbox"
|
||||||
|
title="Threshold Tools"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedDynamicExport = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DynamicExport
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'dynamic-volume',
|
||||||
|
iconName: 'group-layers',
|
||||||
|
iconLabel: '4D Workflow',
|
||||||
|
label: '4D Workflow',
|
||||||
|
component: wrappedDynamicDataPanel,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dynamic-toolbox',
|
||||||
|
iconName: 'group-layers',
|
||||||
|
iconLabel: '4D Workflow',
|
||||||
|
label: 'Dynamic Toolbox',
|
||||||
|
component: wrappedDynamicToolbox,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dynamic-export',
|
||||||
|
iconName: 'group-layers',
|
||||||
|
iconLabel: '4D Workflow',
|
||||||
|
label: '4D Workflow',
|
||||||
|
component: wrappedDynamicExport,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getPanelModule;
|
||||||
6
extensions/cornerstone-dynamic-volume/src/id.js
Normal file
6
extensions/cornerstone-dynamic-volume/src/id.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import packageJson from '../package.json';
|
||||||
|
|
||||||
|
const id = packageJson.name;
|
||||||
|
const SOPClassHandlerName = 'dynamic-volume';
|
||||||
|
|
||||||
|
export { id, SOPClassHandlerName };
|
||||||
57
extensions/cornerstone-dynamic-volume/src/index.ts
Normal file
57
extensions/cornerstone-dynamic-volume/src/index.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { id } from './id';
|
||||||
|
import commandsModule from './commandsModule';
|
||||||
|
import getPanelModule from './getPanelModule';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
|
import { cache } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can remove any of the following modules if you don't need them.
|
||||||
|
*/
|
||||||
|
const dynamicVolumeExtension = {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
* You ID can be anything you want, but it should be unique.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform any pre-registration tasks here. This is called before the extension
|
||||||
|
* is registered. Usually we run tasks such as: configuring the libraries
|
||||||
|
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
|
||||||
|
* this extension is providing.
|
||||||
|
*/
|
||||||
|
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {
|
||||||
|
// TODO: look for the right fix
|
||||||
|
cache.setMaxCacheSize(5 * 1024 * 1024 * 1024);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* PanelModule should provide a list of panels that will be available in OHIF
|
||||||
|
* for Modes to consume and render. Each panel is defined by a {name,
|
||||||
|
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||||
|
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||||
|
*/
|
||||||
|
getPanelModule,
|
||||||
|
/**
|
||||||
|
* ViewportModule should provide a list of viewports that will be available in OHIF
|
||||||
|
* for Modes to consume and use in the viewports. Each viewport is defined by
|
||||||
|
* {name, component} object. Example of a viewport module is the CornerstoneViewport
|
||||||
|
* that is provided by the Cornerstone extension in OHIF.
|
||||||
|
*/
|
||||||
|
getHangingProtocolModule,
|
||||||
|
/**
|
||||||
|
* CommandsModule should provide a list of commands that will be available in OHIF
|
||||||
|
* for Modes to consume and use in the viewports. Each command is defined by
|
||||||
|
* an object of { actions, definitions, defaultContext } where actions is an
|
||||||
|
* object of functions, definitions is an object of available commands, their
|
||||||
|
* options, and defaultContext is the default context for the command to run against.
|
||||||
|
*/
|
||||||
|
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {
|
||||||
|
return commandsModule({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dynamicVolumeExtension as default };
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PanelGenerateImage from './PanelGenerateImage';
|
||||||
|
|
||||||
|
function DynamicDataPanel({ servicesManager, commandsManager }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-auto flex-col text-white"
|
||||||
|
data-cy={'dynamic-volume-panel'}
|
||||||
|
>
|
||||||
|
<PanelGenerateImage
|
||||||
|
commandsManager={commandsManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
></PanelGenerateImage>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicDataPanel;
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { ActionButtons } from '@ohif/ui';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
function DynamicExport({ commandsManager, servicesManager, extensionManager }) {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
const { t } = useTranslation('dynamicExport');
|
||||||
|
|
||||||
|
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
label: 'Export Time Data',
|
||||||
|
onClick: () => {
|
||||||
|
commandsManager.runCommand('exportTimeReportCSV', {
|
||||||
|
segmentations,
|
||||||
|
options: {
|
||||||
|
filename: 'TimeData.csv',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
disabled: !segmentations?.length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Export ROI Stats',
|
||||||
|
onClick: () => {
|
||||||
|
commandsManager.runCommand('exportTimeReportCSV', {
|
||||||
|
segmentations,
|
||||||
|
summaryStats: true,
|
||||||
|
options: {
|
||||||
|
filename: 'ROIStats.csv',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
disabled: !segmentations?.length,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI based on segmentation changes (added, removed, updated)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
// ~~ Subscription
|
||||||
|
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||||
|
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||||
|
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||||
|
const subscriptions = [];
|
||||||
|
|
||||||
|
[added, updated, removed].forEach(evt => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
setSegmentations(segmentations);
|
||||||
|
});
|
||||||
|
subscriptions.push(unsubscribe);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(unsub => {
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mt-3 flex justify-center px-2">
|
||||||
|
<ActionButtons
|
||||||
|
actions={actions}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicExport;
|
||||||
@ -0,0 +1,220 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
InputDoubleRange,
|
||||||
|
Button,
|
||||||
|
PanelSection,
|
||||||
|
ButtonGroup,
|
||||||
|
IconButton,
|
||||||
|
InputNumber,
|
||||||
|
Icon,
|
||||||
|
Tooltip,
|
||||||
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const controlClassNames = {
|
||||||
|
sizeClassName: 'w-[58px] h-[28px]',
|
||||||
|
arrowsDirection: 'horizontal',
|
||||||
|
labelPosition: 'bottom',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = ({ title, tooltip }) => (
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
<Tooltip
|
||||||
|
content={<div className="text-white">{tooltip}</div>}
|
||||||
|
position="bottom"
|
||||||
|
tight={true}
|
||||||
|
tooltipBoxClassName="max-w-xs"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="info-link"
|
||||||
|
className="text-primary-active h-[14px] w-[14px]"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DynamicVolumeControls = ({
|
||||||
|
isPlaying,
|
||||||
|
onPlayPauseChange,
|
||||||
|
// fps
|
||||||
|
fps,
|
||||||
|
onFpsChange,
|
||||||
|
minFps,
|
||||||
|
maxFps,
|
||||||
|
// Frames
|
||||||
|
currentFrameIndex,
|
||||||
|
onFrameChange,
|
||||||
|
framesLength,
|
||||||
|
onGenerate,
|
||||||
|
onDoubleRangeChange,
|
||||||
|
onDynamicClick,
|
||||||
|
}) => {
|
||||||
|
const [computedView, setComputedView] = useState(false);
|
||||||
|
|
||||||
|
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||||
|
|
||||||
|
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
||||||
|
}, [framesLength]);
|
||||||
|
|
||||||
|
const handleSliderChange = newValues => {
|
||||||
|
onDoubleRangeChange(newValues);
|
||||||
|
|
||||||
|
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSliderRangeValues(newValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex select-none flex-col">
|
||||||
|
<PanelSection
|
||||||
|
title="Controls"
|
||||||
|
childrenClassName="space-y-4 pb-5 px-5"
|
||||||
|
>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Header
|
||||||
|
title="View"
|
||||||
|
tooltip={
|
||||||
|
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ButtonGroup className="mt-2 w-full">
|
||||||
|
<button
|
||||||
|
className="w-1/2"
|
||||||
|
onClick={() => {
|
||||||
|
setComputedView(false);
|
||||||
|
onDynamicClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
4D
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-1/2"
|
||||||
|
onClick={() => {
|
||||||
|
setComputedView(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Computed
|
||||||
|
</button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FrameControls
|
||||||
|
onPlayPauseChange={onPlayPauseChange}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
computedView={computedView}
|
||||||
|
// fps
|
||||||
|
fps={fps}
|
||||||
|
onFpsChange={onFpsChange}
|
||||||
|
minFps={minFps}
|
||||||
|
maxFps={maxFps}
|
||||||
|
//
|
||||||
|
framesLength={framesLength}
|
||||||
|
onFrameChange={onFrameChange}
|
||||||
|
currentFrameIndex={currentFrameIndex}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||||
|
<Header title="Computed Operation" />
|
||||||
|
<ButtonGroup
|
||||||
|
className={`mt-2 w-full `}
|
||||||
|
separated={true}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="w-1/2"
|
||||||
|
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
|
||||||
|
>
|
||||||
|
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-1/2"
|
||||||
|
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
|
||||||
|
>
|
||||||
|
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-1/2"
|
||||||
|
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
|
||||||
|
>
|
||||||
|
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||||
|
</button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<div className="w-ful mt-2">
|
||||||
|
<InputDoubleRange
|
||||||
|
values={sliderRangeValues}
|
||||||
|
onChange={handleSliderChange}
|
||||||
|
minValue={0}
|
||||||
|
showLabel={true}
|
||||||
|
allowNumberEdit={true}
|
||||||
|
maxValue={framesLength}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
||||||
|
onClick={() => {
|
||||||
|
onGenerate(computeViewMode);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PanelSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicVolumeControls;
|
||||||
|
|
||||||
|
function FrameControls({
|
||||||
|
isPlaying,
|
||||||
|
onPlayPauseChange,
|
||||||
|
fps,
|
||||||
|
minFps,
|
||||||
|
maxFps,
|
||||||
|
onFpsChange,
|
||||||
|
framesLength,
|
||||||
|
onFrameChange,
|
||||||
|
currentFrameIndex,
|
||||||
|
computedView,
|
||||||
|
}) {
|
||||||
|
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={computedView && 'ohif-disabled'}>
|
||||||
|
<Header title="4D Controls" />
|
||||||
|
<div className="mt-3 flex justify-between">
|
||||||
|
<IconButton
|
||||||
|
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
||||||
|
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={getPlayPauseIconName()}
|
||||||
|
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<InputNumber
|
||||||
|
value={currentFrameIndex}
|
||||||
|
onChange={onFrameChange}
|
||||||
|
minValue={0}
|
||||||
|
maxValue={framesLength}
|
||||||
|
label="Frame"
|
||||||
|
{...controlClassNames}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
value={fps}
|
||||||
|
onChange={onFpsChange}
|
||||||
|
minValue={minFps}
|
||||||
|
maxValue={maxFps}
|
||||||
|
{...controlClassNames}
|
||||||
|
label="FPS"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { InputDoubleRange } from '@ohif/ui';
|
||||||
|
import { Select } from '@ohif/ui';
|
||||||
|
import { Button } from '@ohif/ui';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const GenerateVolume = ({
|
||||||
|
rangeValues,
|
||||||
|
handleSliderChange,
|
||||||
|
operationsUI,
|
||||||
|
options,
|
||||||
|
handleGenerateOptionsChange,
|
||||||
|
onGenerateImage,
|
||||||
|
returnTo4D,
|
||||||
|
displayingComputedVolume,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 text-white">Computed Image</div>
|
||||||
|
<Select
|
||||||
|
closeMenuOnSelect={true}
|
||||||
|
className="border-primary-main mr-2 bg-black text-white "
|
||||||
|
options={operationsUI}
|
||||||
|
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
|
||||||
|
value={options.Operation}
|
||||||
|
onChange={({ value }) => {
|
||||||
|
handleGenerateOptionsChange({
|
||||||
|
Operation: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputDoubleRange
|
||||||
|
values={rangeValues}
|
||||||
|
onChange={handleSliderChange}
|
||||||
|
minValue={rangeValues[0] || 1}
|
||||||
|
maxValue={rangeValues[1] || 2}
|
||||||
|
showLabel={true}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
onClick={onGenerateImage}
|
||||||
|
className="w-1/2"
|
||||||
|
>
|
||||||
|
Generate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={returnTo4D}
|
||||||
|
disabled={!displayingComputedVolume}
|
||||||
|
className="w-1/2"
|
||||||
|
>
|
||||||
|
Return To 4D
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
GenerateVolume.propTypes = {
|
||||||
|
rangeValues: PropTypes.array.isRequired,
|
||||||
|
handleSliderChange: PropTypes.func.isRequired,
|
||||||
|
operationsUI: PropTypes.array.isRequired,
|
||||||
|
options: PropTypes.object.isRequired,
|
||||||
|
handleGenerateOptionsChange: PropTypes.func.isRequired,
|
||||||
|
onGenerateImage: PropTypes.func.isRequired,
|
||||||
|
returnTo4D: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GenerateVolume;
|
||||||
@ -0,0 +1,275 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||||
|
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
||||||
|
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
|
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||||
|
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||||
|
|
||||||
|
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
|
||||||
|
|
||||||
|
export default function PanelGenerateImage({ servicesManager, commandsManager }) {
|
||||||
|
const { cornerstoneViewportService, viewportGridService, displaySetService } =
|
||||||
|
servicesManager.services;
|
||||||
|
|
||||||
|
const [{ isCineEnabled }, cineService] = useCine();
|
||||||
|
const [{ activeViewportId }] = useViewportGrid();
|
||||||
|
|
||||||
|
//
|
||||||
|
const [timePointsRange, setTimePointsRange] = useState([]);
|
||||||
|
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
||||||
|
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||||
|
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||||
|
const [frameRate, setFrameRate] = useState(20);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
|
||||||
|
const [timePointRendered, setTimePointRendered] = useState(null);
|
||||||
|
const [displayingComputed, setDisplayingComputed] = useState(false);
|
||||||
|
|
||||||
|
//
|
||||||
|
const uuidComputedVolume = useRef(csUtils.uuidv4());
|
||||||
|
const uuidDynamicVolume = useRef(null);
|
||||||
|
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||||
|
|
||||||
|
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
|
||||||
|
evtDetails.viewportData.data.forEach(volumeData => {
|
||||||
|
if (volumeData.volume.isDynamicVolume()) {
|
||||||
|
setDynamicVolume(volumeData.volume);
|
||||||
|
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||||
|
setTimePointsRange([1, volumeData.volume.numTimePoints]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [cornerstoneViewportService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
||||||
|
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
||||||
|
evt => {
|
||||||
|
setIsPlaying(evt.isPlaying);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [cineService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||||
|
|
||||||
|
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displaySets = displaySetUIDs.map(displaySetUID =>
|
||||||
|
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||||
|
);
|
||||||
|
|
||||||
|
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||||
|
|
||||||
|
if (!dynamicVolumeDisplaySet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicVolume = cache
|
||||||
|
.getVolumes()
|
||||||
|
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
|
||||||
|
|
||||||
|
if (!dynamicVolume) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDynamicVolume(dynamicVolume);
|
||||||
|
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||||
|
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
||||||
|
}, [activeViewportId, cornerstoneViewportService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// ~~ Subscription
|
||||||
|
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||||
|
|
||||||
|
const callback = evt => {
|
||||||
|
setTimePointRendered(evt.detail.timePointIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventTarget.addEventListener(evt, callback);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventTarget.removeEventListener(evt, callback);
|
||||||
|
};
|
||||||
|
}, [cornerstoneViewportService]);
|
||||||
|
|
||||||
|
function renderGeneratedImage(displaySet) {
|
||||||
|
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||||
|
displaySet,
|
||||||
|
});
|
||||||
|
|
||||||
|
setDisplayingComputed(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDynamicImage(displaySet) {
|
||||||
|
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get computed volume from cache, calculate the data across the time frames,
|
||||||
|
// set the scalar data to the computedVolume, and create displaySet
|
||||||
|
async function onGenerateImage(operationName) {
|
||||||
|
const dynamicVolumeId = dynamicVolume.volumeId;
|
||||||
|
|
||||||
|
if (!dynamicVolumeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let computedVolume = cache.getVolume(computedVolumeId);
|
||||||
|
|
||||||
|
if (!computedVolume) {
|
||||||
|
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
||||||
|
computedVolume = cache.getVolume(computedVolumeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vals = timePointsRangeToUseForGenerate;
|
||||||
|
|
||||||
|
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
|
||||||
|
|
||||||
|
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
|
||||||
|
dynamicVolume,
|
||||||
|
operationName,
|
||||||
|
operationName === 'SUBTRACT' ? vals : targets
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add loadStatus.loaded to computed volume and set to true
|
||||||
|
computedVolume.loadStatus = {};
|
||||||
|
computedVolume.loadStatus.loaded = true;
|
||||||
|
// Set computed scalar data to volume
|
||||||
|
const scalarData = computedVolume.getScalarData();
|
||||||
|
scalarData.set(dataInTime);
|
||||||
|
|
||||||
|
// If computed display set does not exist, create an object to be used as
|
||||||
|
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||||
|
if (!computedDisplaySet) {
|
||||||
|
const displaySet = {
|
||||||
|
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
|
||||||
|
displaySetInstanceUID: uuidComputedVolume.current,
|
||||||
|
SOPClassHandlerId: SOPClassHandlerId,
|
||||||
|
Modality: dynamicVolume.metadata.Modality,
|
||||||
|
isMultiFrame: false,
|
||||||
|
numImageFrames: 1,
|
||||||
|
uid: uuidComputedVolume.current,
|
||||||
|
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
|
||||||
|
madeInClient: true,
|
||||||
|
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||||
|
isDerived: true,
|
||||||
|
};
|
||||||
|
setComputedDisplaySet(displaySet);
|
||||||
|
renderGeneratedImage(displaySet);
|
||||||
|
} else {
|
||||||
|
commandsManager.runCommand('updateVolumeData', {
|
||||||
|
volume: computedVolume,
|
||||||
|
});
|
||||||
|
// Check if viewport is currently displaying the computed volume, if so,
|
||||||
|
// call render on the viewports to update the image, if not, call
|
||||||
|
// renderGeneratedImage
|
||||||
|
// if (!cache.getVolume(dynamicVolumeId)) {
|
||||||
|
// for (const viewportId of viewports.keys()) {
|
||||||
|
// const viewportForRendering =
|
||||||
|
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
// viewportForRendering.render();
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
cornerstoneViewportService.getRenderingEngine().render();
|
||||||
|
renderGeneratedImage(computedDisplaySet);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPlayPauseChange = isPlaying => {
|
||||||
|
isPlaying ? handlePlay() : handleStop();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||||
|
|
||||||
|
if (!viewportInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { element } = viewportInfo;
|
||||||
|
cineService.playClip(element, { framesPerSecond: frameRate });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStop = () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||||
|
cineService.stopClip(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetFrameRate = newFrameRate => {
|
||||||
|
setFrameRate(newFrameRate);
|
||||||
|
handleStop();
|
||||||
|
handlePlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleSliderChange(newValues) {
|
||||||
|
if (
|
||||||
|
newValues[0] === timePointsRangeToUseForGenerate[0] &&
|
||||||
|
newValues[1] === timePointsRangeToUseForGenerate[1]
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimePointsRangeToUseForGenerate(newValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dynamicVolume || timePointsRange.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicVolumeControls
|
||||||
|
fps={frameRate}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onPlayPauseChange={onPlayPauseChange}
|
||||||
|
minFps={1}
|
||||||
|
maxFps={50}
|
||||||
|
currentFrameIndex={timePointRendered}
|
||||||
|
onFpsChange={handleSetFrameRate}
|
||||||
|
framesLength={timePointsRange[1]}
|
||||||
|
onFrameChange={timePointIndex => {
|
||||||
|
dynamicVolume.timePointIndex = timePointIndex;
|
||||||
|
}}
|
||||||
|
onGenerate={onGenerateImage}
|
||||||
|
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||||
|
onDoubleRangeChange={handleSliderChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
||||||
|
if (!cache.getVolume(computedVolumeId)) {
|
||||||
|
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||||
|
volumeId: computedVolumeId,
|
||||||
|
});
|
||||||
|
return computedVolume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelGenerateImage.propTypes = {
|
||||||
|
servicesManager: PropTypes.shape({
|
||||||
|
services: PropTypes.shape({
|
||||||
|
measurementService: PropTypes.shape({
|
||||||
|
getMeasurements: PropTypes.func.isRequired,
|
||||||
|
subscribe: PropTypes.func.isRequired,
|
||||||
|
EVENTS: PropTypes.object.isRequired,
|
||||||
|
VALUE_TYPES: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
function WorkflowPanel({ servicesManager, extensionManager }) {
|
||||||
|
const ProgressDropdownWithService = useMemo(() => {
|
||||||
|
const defaultComponents = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-default.customizationModule.default'
|
||||||
|
).value;
|
||||||
|
|
||||||
|
return defaultComponents.find(
|
||||||
|
component => component.id === 'progressDropdownWithServiceComponent'
|
||||||
|
).component;
|
||||||
|
}, [extensionManager]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-cy={'workflow-panel'}
|
||||||
|
className="bg-secondary-dark mb-1 px-3 py-4"
|
||||||
|
>
|
||||||
|
<div className="mb-1">Workflow</div>
|
||||||
|
<div>
|
||||||
|
<ProgressDropdownWithService servicesManager={servicesManager} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkflowPanel;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import DynamicDataPanel from './DynamicDataPanel';
|
||||||
|
import WorkflowPanel from './WorkflowPanel';
|
||||||
|
import PanelGenerateImage from './PanelGenerateImage';
|
||||||
|
|
||||||
|
export { DynamicDataPanel, WorkflowPanel, PanelGenerateImage };
|
||||||
@ -38,7 +38,7 @@
|
|||||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||||
"@cornerstonejs/dicom-image-loader": "^1.68.1",
|
"@cornerstonejs/dicom-image-loader": "^1.70.0",
|
||||||
"@icr/polyseg-wasm": "^0.4.0",
|
"@icr/polyseg-wasm": "^0.4.0",
|
||||||
"@ohif/core": "3.8.0-beta.73",
|
"@ohif/core": "3.8.0-beta.73",
|
||||||
"@ohif/ui": "3.8.0-beta.73",
|
"@ohif/ui": "3.8.0-beta.73",
|
||||||
@ -55,11 +55,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
"@cornerstonejs/adapters": "^1.68.1",
|
"@cornerstonejs/adapters": "^1.70.0",
|
||||||
"@cornerstonejs/core": "^1.68.1",
|
"@cornerstonejs/core": "^1.70.0",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^1.68.1",
|
"@cornerstonejs/streaming-image-volume-loader": "^1.70.0",
|
||||||
"@cornerstonejs/tools": "^1.68.1",
|
"@cornerstonejs/tools": "^1.70.0",
|
||||||
"@kitware/vtk.js": "29.7.0",
|
"@icr/polyseg-wasm": "^0.4.0",
|
||||||
|
"@kitware/vtk.js": "30.3.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
|
|||||||
@ -118,6 +118,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const viewportId = viewportOptions.viewportId;
|
const viewportId = viewportOptions.viewportId;
|
||||||
|
|
||||||
|
// Since we only have support for dynamic data in volume viewports, we should
|
||||||
|
// handle this case here and set the viewportType to volume if any of the
|
||||||
|
// displaySets are dynamic volumes
|
||||||
|
viewportOptions.viewportType = displaySets.some(ds => ds.isDynamicVolume)
|
||||||
|
? 'volume'
|
||||||
|
: viewportOptions.viewportType;
|
||||||
|
|
||||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||||
const elementRef = useRef();
|
const elementRef = useRef();
|
||||||
|
|||||||
@ -322,7 +322,7 @@ function _getViewportInstances(viewportData) {
|
|||||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||||
const volumes = viewportData.data;
|
const volumes = viewportData.data;
|
||||||
volumes.forEach(volume => {
|
volumes.forEach(volume => {
|
||||||
if (!volume?.imageIds) {
|
if (!volume?.imageIds || volume.imageIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
imageIds.push(volume.imageIds[0]);
|
imageIds.push(volume.imageIds[0]);
|
||||||
|
|||||||
@ -126,16 +126,12 @@ function commandsModule({
|
|||||||
? nearbyToolData
|
? nearbyToolData
|
||||||
: null;
|
: null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Measurement tool commands:
|
|
||||||
|
|
||||||
/** Delete the given measurement */
|
/** Delete the given measurement */
|
||||||
deleteMeasurement: ({ uid }) => {
|
deleteMeasurement: ({ uid }) => {
|
||||||
if (uid) {
|
if (uid) {
|
||||||
measurementServiceSource.remove(uid);
|
measurementServiceSource.remove(uid);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the measurement labelling input dialog and update the label
|
* Show the measurement labelling input dialog and update the label
|
||||||
* on the measurement with a response if not cancelled.
|
* on the measurement with a response if not cancelled.
|
||||||
@ -302,6 +298,43 @@ function commandsModule({
|
|||||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
renderingEngine.render();
|
renderingEngine.render();
|
||||||
},
|
},
|
||||||
|
toggleEnabledDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||||
|
const toolName = itemId || value;
|
||||||
|
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
|
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolIsEnabled = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Enabled;
|
||||||
|
|
||||||
|
toolIsEnabled ? toolGroup.setToolDisabled(toolName) : toolGroup.setToolEnabled(toolName);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleActiveDisabledToolbar({ value, itemId, toolGroupIds = [] }) {
|
||||||
|
const toolName = itemId || value;
|
||||||
|
toolGroupIds = toolGroupIds.length ? toolGroupIds : toolGroupService.getToolGroupIds();
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||||
|
if (!toolGroup || !toolGroup.hasTool(toolName)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolIsActive = toolGroup.getToolOptions(toolName).mode === Enums.ToolModes.Active;
|
||||||
|
|
||||||
|
toolIsActive
|
||||||
|
? toolGroup.setToolDisabled(toolName)
|
||||||
|
: actions.setToolActive({ toolName, toolGroupId });
|
||||||
|
|
||||||
|
// we should set the previously active tool to active after we set the
|
||||||
|
// current tool disabled
|
||||||
|
if (toolIsActive) {
|
||||||
|
const prevToolName = toolGroup.getPrevActivePrimaryToolName();
|
||||||
|
actions.setToolActive({ toolName: prevToolName, toolGroupId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
setToolActiveToolbar: ({ value, itemId, toolGroupIds = [] }) => {
|
||||||
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
|
||||||
const toolName = itemId || value;
|
const toolName = itemId || value;
|
||||||
@ -639,6 +672,17 @@ function commandsModule({
|
|||||||
storePresentation: ({ viewportId }) => {
|
storePresentation: ({ viewportId }) => {
|
||||||
cornerstoneViewportService.storePresentation({ viewportId });
|
cornerstoneViewportService.storePresentation({ viewportId });
|
||||||
},
|
},
|
||||||
|
updateVolumeData: ({ volume }) => {
|
||||||
|
// update vtkOpenGLTexture and imageData of computed volume
|
||||||
|
const { imageData, vtkOpenGLTexture } = volume;
|
||||||
|
const numSlices = imageData.getDimensions()[2];
|
||||||
|
const slicesToUpdate = [...Array(numSlices).keys()];
|
||||||
|
slicesToUpdate.forEach(i => {
|
||||||
|
vtkOpenGLTexture.setUpdatedFrame(i);
|
||||||
|
});
|
||||||
|
imageData.modified();
|
||||||
|
},
|
||||||
|
|
||||||
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
|
attachProtocolViewportDataListener: ({ protocol, stageIndex }) => {
|
||||||
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||||
const command = protocol.callbacks.onViewportDataInitialized;
|
const command = protocol.callbacks.onViewportDataInitialized;
|
||||||
@ -928,6 +972,15 @@ function commandsModule({
|
|||||||
toggleSynchronizer: {
|
toggleSynchronizer: {
|
||||||
commandFn: actions.toggleSynchronizer,
|
commandFn: actions.toggleSynchronizer,
|
||||||
},
|
},
|
||||||
|
updateVolumeData: {
|
||||||
|
commandFn: actions.updateVolumeData,
|
||||||
|
},
|
||||||
|
toggleEnabledDisabledToolbar: {
|
||||||
|
commandFn: actions.toggleEnabledDisabledToolbar,
|
||||||
|
},
|
||||||
|
toggleActiveDisabledToolbar: {
|
||||||
|
commandFn: actions.toggleActiveDisabledToolbar,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { ServicesManager } from '@ohif/core';
|
||||||
|
import { useViewportGrid } from '@ohif/ui';
|
||||||
|
import ViewportWindowLevel from '../ViewportWindowLevel/ViewportWindowLevel';
|
||||||
|
|
||||||
|
const ActiveViewportWindowLevel = ({
|
||||||
|
servicesManager,
|
||||||
|
}: {
|
||||||
|
servicesManager: ServicesManager;
|
||||||
|
}): ReactElement => {
|
||||||
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
const { activeViewportId } = viewportGrid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{activeViewportId && (
|
||||||
|
<ViewportWindowLevel
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
viewportId={activeViewportId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ActiveViewportWindowLevel.propTypes = {
|
||||||
|
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActiveViewportWindowLevel;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ActiveViewportWindowLevel';
|
||||||
@ -1,73 +1,108 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useState, useRef } from 'react';
|
||||||
import { CinePlayer, useCine } from '@ohif/ui';
|
import { CinePlayer, useCine } from '@ohif/ui';
|
||||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
import { Enums, eventTarget, cache } from '@cornerstonejs/core';
|
||||||
|
import { Enums as StreamingEnums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
|
||||||
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||||
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
const { customizationService, displaySetService, viewportGridService } = servicesManager.services;
|
||||||
const [{ isCineEnabled, cines }, api] = useCine();
|
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||||
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
|
const [newStackFrameRate, setNewStackFrameRate] = useState(24);
|
||||||
|
const [dynamicInfo, setDynamicInfo] = useState(null);
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
const isMountedRef = useRef(null);
|
const isMountedRef = useRef(null);
|
||||||
|
|
||||||
const { component: CinePlayerComponent = CinePlayer } =
|
|
||||||
customizationService.get('cinePlayer') ?? {};
|
|
||||||
|
|
||||||
const cineHandler = () => {
|
const cineHandler = () => {
|
||||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
if (!cines?.[viewportId] || !enabledVPElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cine = cines[viewportId];
|
const { isPlaying = false, frameRate = 24 } = cines[viewportId];
|
||||||
const isPlaying = cine.isPlaying || false;
|
|
||||||
const frameRate = cine.frameRate || 24;
|
|
||||||
|
|
||||||
const validFrameRate = Math.max(frameRate, 1);
|
const validFrameRate = Math.max(frameRate, 1);
|
||||||
|
|
||||||
if (isPlaying) {
|
return isPlaying
|
||||||
api.playClip(enabledVPElement, {
|
? cineService.playClip(enabledVPElement, { framesPerSecond: validFrameRate })
|
||||||
framesPerSecond: validFrameRate,
|
: cineService.stopClip(enabledVPElement);
|
||||||
});
|
|
||||||
} else {
|
|
||||||
api.stopClip(enabledVPElement);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newStackCineHandler = useCallback(() => {
|
const newDisplaySetHandler = useCallback(() => {
|
||||||
|
if (!enabledVPElement || !isCineEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { viewports } = viewportGridService.getState();
|
const { viewports } = viewportGridService.getState();
|
||||||
const { displaySetInstanceUIDs } = viewports.get(viewportId);
|
const { displaySetInstanceUIDs } = viewports.get(viewportId);
|
||||||
|
|
||||||
let frameRate = 24;
|
let frameRate = 24;
|
||||||
let isPlaying = cines[viewportId].isPlaying;
|
let isPlaying = cines[viewportId]?.isPlaying || false;
|
||||||
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||||
|
|
||||||
if (displaySet.FrameRate) {
|
if (displaySet.FrameRate) {
|
||||||
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
|
// displaySet.FrameRate corresponds to DICOM tag (0018,1063) which is defined as the the frame time in milliseconds
|
||||||
// So a bit of math to get the actual frame rate.
|
// So a bit of math to get the actual frame rate.
|
||||||
frameRate = Math.round(1000 / displaySet.FrameRate);
|
frameRate = Math.round(1000 / displaySet.FrameRate);
|
||||||
isPlaying ||= !!appConfig.autoPlayCine;
|
isPlaying ||= !!appConfig.autoPlayCine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if the displaySet is dynamic and set the dynamic info
|
||||||
|
if (displaySet.isDynamicVolume) {
|
||||||
|
const { dynamicVolumeInfo } = displaySet;
|
||||||
|
const numTimePoints = dynamicVolumeInfo.timePoints.length;
|
||||||
|
const label = dynamicVolumeInfo.splittingTag;
|
||||||
|
const timePointIndex = dynamicVolumeInfo.timePointIndex || 0;
|
||||||
|
setDynamicInfo({
|
||||||
|
volumeId: displaySet.displaySetInstanceUID,
|
||||||
|
timePointIndex,
|
||||||
|
numTimePoints,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setDynamicInfo(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
api.setIsCineEnabled(isPlaying);
|
cineService.setIsCineEnabled(isPlaying);
|
||||||
}
|
}
|
||||||
api.setCine({ id: viewportId, isPlaying, frameRate });
|
cineService.setCine({ id: viewportId, isPlaying, frameRate });
|
||||||
setNewStackFrameRate(frameRate);
|
setNewStackFrameRate(frameRate);
|
||||||
}, [displaySetService, viewportId, viewportGridService, cines]);
|
}, [displaySetService, viewportId, viewportGridService, cines, isCineEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isMountedRef.current = true;
|
isMountedRef.current = true;
|
||||||
|
|
||||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
newDisplaySetHandler();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
api.stopClip(enabledVPElement);
|
|
||||||
api.setCine({ id: viewportId, isPlaying: false });
|
|
||||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newStackCineHandler);
|
|
||||||
};
|
};
|
||||||
}, [enabledVPElement, newStackCineHandler]);
|
}, [isCineEnabled, newDisplaySetHandler]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use effect for handling new display set
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabledVPElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
||||||
|
// this doesn't makes sense that we are listening to this event on viewport element
|
||||||
|
enabledVPElement.addEventListener(
|
||||||
|
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||||
|
newDisplaySetHandler
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||||
|
|
||||||
|
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, newDisplaySetHandler);
|
||||||
|
enabledVPElement.removeEventListener(
|
||||||
|
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||||
|
newDisplaySetHandler
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [enabledVPElement, newDisplaySetHandler, viewportId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
|
if (!cines || !cines[viewportId] || !enabledVPElement || !isMountedRef.current) {
|
||||||
@ -77,41 +112,118 @@ function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
|||||||
cineHandler();
|
cineHandler();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
api.stopClip(enabledVPElement);
|
cineService.stopClip(enabledVPElement);
|
||||||
};
|
};
|
||||||
}, [cines, viewportId, enabledVPElement, cineHandler]);
|
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
||||||
|
|
||||||
|
if (!isCineEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const cine = cines[viewportId];
|
const cine = cines[viewportId];
|
||||||
const isPlaying = (cine && cine.isPlaying) || false;
|
const isPlaying = cine?.isPlaying || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isCineEnabled && (
|
<RenderCinePlayer
|
||||||
<CinePlayerComponent
|
viewportId={viewportId}
|
||||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
cineService={cineService}
|
||||||
frameRate={newStackFrameRate}
|
newStackFrameRate={newStackFrameRate}
|
||||||
isPlaying={isPlaying}
|
isPlaying={isPlaying}
|
||||||
onClose={() => {
|
dynamicInfo={dynamicInfo}
|
||||||
// also stop the clip
|
customizationService={customizationService}
|
||||||
api.setCine({
|
/>
|
||||||
id: viewportId,
|
);
|
||||||
isPlaying: false,
|
}
|
||||||
});
|
|
||||||
api.setIsCineEnabled(false);
|
function RenderCinePlayer({
|
||||||
}}
|
viewportId,
|
||||||
onPlayPauseChange={isPlaying => {
|
cineService,
|
||||||
api.setCine({
|
newStackFrameRate,
|
||||||
id: viewportId,
|
isPlaying,
|
||||||
isPlaying,
|
dynamicInfo: dynamicInfoProp,
|
||||||
});
|
customizationService,
|
||||||
}}
|
}) {
|
||||||
onFrameRateChange={frameRate =>
|
const { component: CinePlayerComponent = CinePlayer } =
|
||||||
api.setCine({
|
customizationService.get('cinePlayer') ?? {};
|
||||||
id: viewportId,
|
|
||||||
frameRate,
|
const [dynamicInfo, setDynamicInfo] = useState(dynamicInfoProp);
|
||||||
})
|
|
||||||
}
|
useEffect(() => {
|
||||||
/>
|
setDynamicInfo(dynamicInfoProp);
|
||||||
)
|
}, [dynamicInfoProp]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use effect for handling 4D time index changed
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dynamicInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTimePointIndexChange = evt => {
|
||||||
|
const { volumeId, timePointIndex, numTimePoints, splittingTag } = evt.detail;
|
||||||
|
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||||
|
};
|
||||||
|
|
||||||
|
eventTarget.addEventListener(
|
||||||
|
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||||
|
handleTimePointIndexChange
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventTarget.removeEventListener(
|
||||||
|
StreamingEnums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED,
|
||||||
|
handleTimePointIndexChange
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [dynamicInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dynamicInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { volumeId, timePointIndex, numTimePoints, splittingTag } = dynamicInfo || {};
|
||||||
|
const volume = cache.getVolume(volumeId);
|
||||||
|
volume.timePointIndex = timePointIndex;
|
||||||
|
|
||||||
|
setDynamicInfo({ volumeId, timePointIndex, numTimePoints, label: splittingTag });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateDynamicInfo = useCallback(props => {
|
||||||
|
const { volumeId, timePointIndex } = props;
|
||||||
|
const volume = cache.getVolume(volumeId);
|
||||||
|
volume.timePointIndex = timePointIndex;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CinePlayerComponent
|
||||||
|
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||||
|
frameRate={newStackFrameRate}
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
onClose={() => {
|
||||||
|
// also stop the clip
|
||||||
|
cineService.setCine({
|
||||||
|
id: viewportId,
|
||||||
|
isPlaying: false,
|
||||||
|
});
|
||||||
|
cineService.setIsCineEnabled(false);
|
||||||
|
}}
|
||||||
|
onPlayPauseChange={isPlaying => {
|
||||||
|
cineService.setCine({
|
||||||
|
id: viewportId,
|
||||||
|
isPlaying,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onFrameRateChange={frameRate =>
|
||||||
|
cineService.setCine({
|
||||||
|
id: viewportId,
|
||||||
|
frameRate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dynamicInfo={dynamicInfo}
|
||||||
|
updateDynamicInfo={updateDynamicInfo}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,387 @@
|
|||||||
|
import React, { useEffect, useCallback, useState, ReactElement } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { ServicesManager } from '@ohif/core';
|
||||||
|
import { PanelSection, WindowLevel } from '@ohif/ui';
|
||||||
|
import vtkColorMaps from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction/ColorMaps';
|
||||||
|
import { Enums, eventTarget, cache as cs3DCache, utilities as csUtils } from '@cornerstonejs/core';
|
||||||
|
import { getViewportVolumeHistogram } from './getViewportVolumeHistogram';
|
||||||
|
|
||||||
|
const { Events } = Enums;
|
||||||
|
|
||||||
|
const ViewportWindowLevel = ({
|
||||||
|
servicesManager,
|
||||||
|
viewportId,
|
||||||
|
}: {
|
||||||
|
servicesManager: ServicesManager;
|
||||||
|
viewportId: string;
|
||||||
|
}): ReactElement => {
|
||||||
|
const { cornerstoneViewportService } = servicesManager.services;
|
||||||
|
const [windowLevels, setWindowLevels] = useState([]);
|
||||||
|
const [cachedHistograms, setCachedHistograms] = useState({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks for all viewports that has exactly all volumeIds passed as parameter.
|
||||||
|
*/
|
||||||
|
const getViewportsWithVolumeIds = useCallback(
|
||||||
|
(volumeIds: string[]) => {
|
||||||
|
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||||
|
const viewports = renderingEngine.getVolumeViewports();
|
||||||
|
|
||||||
|
return viewports.filter(vp => {
|
||||||
|
const viewportVolumeIds = vp.getActors().map(actor => actor.uid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
volumeIds.length === viewportVolumeIds.length &&
|
||||||
|
volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[cornerstoneViewportService]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNodeOpacity = (volumeActor, nodeIndex) => {
|
||||||
|
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||||
|
const nodeValue = [];
|
||||||
|
|
||||||
|
volumeOpacity.getNodeValue(nodeIndex, nodeValue);
|
||||||
|
|
||||||
|
return nodeValue[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the opacity applied to the PET volume is something like
|
||||||
|
* [{x: 0, y: 0}, {x: 0.1, y: [C]}, {x: [ANY], y: [C]}] where C is a
|
||||||
|
* constant opacity value for all x's greater than 0.1
|
||||||
|
*/
|
||||||
|
const isPetVolumeWithDefaultOpacity = (volumeId, volumeActor) => {
|
||||||
|
const volume = cs3DCache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (!volume) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modality = volume.metadata.Modality;
|
||||||
|
|
||||||
|
if (modality !== 'PT') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||||
|
|
||||||
|
// It must have at least two points (0 and 0.1)
|
||||||
|
if (volumeOpacity.getSize() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node1Value = [];
|
||||||
|
const node2Value = [];
|
||||||
|
|
||||||
|
volumeOpacity.getNodeValue(0, node1Value);
|
||||||
|
volumeOpacity.getNodeValue(1, node2Value);
|
||||||
|
|
||||||
|
// First node must be (x:0, y:0} and the second one {x:0.1, y:any}
|
||||||
|
if (node1Value[0] !== 0 || node1Value[1] !== 0 || node2Value[0] !== 0.1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedOpacity = node2Value[1];
|
||||||
|
const opacitySize = volumeOpacity.getSize();
|
||||||
|
const currentNodeValue = [];
|
||||||
|
|
||||||
|
// Any point after 0.1 must have the same opacity
|
||||||
|
for (let i = 2; i < opacitySize; i++) {
|
||||||
|
volumeOpacity.getNodeValue(i, currentNodeValue);
|
||||||
|
|
||||||
|
if (currentNodeValue[1] !== expectedOpacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the opacity function has a constance opacity value for all x's
|
||||||
|
*/
|
||||||
|
const isVolumeWithConstantOpacity = volumeActor => {
|
||||||
|
const volumeOpacity = volumeActor.getProperty().getScalarOpacity(0);
|
||||||
|
const opacitySize = volumeOpacity.getSize();
|
||||||
|
const firstNodeValue = [];
|
||||||
|
|
||||||
|
volumeOpacity.getNodeValue(0, firstNodeValue);
|
||||||
|
|
||||||
|
const firstNodeOpacity = firstNodeValue[1];
|
||||||
|
|
||||||
|
for (let i = 0; i < opacitySize; i++) {
|
||||||
|
const currentNodeValue = [];
|
||||||
|
|
||||||
|
volumeOpacity.getNodeValue(0, currentNodeValue);
|
||||||
|
|
||||||
|
if (currentNodeValue[1] !== firstNodeOpacity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVolumeOpacity = useCallback((viewport, volumeId) => {
|
||||||
|
const volumeActor = viewport.getActor(volumeId).actor;
|
||||||
|
|
||||||
|
if (isPetVolumeWithDefaultOpacity(volumeId, volumeActor)) {
|
||||||
|
// Get the opacity from the second node at 0.1
|
||||||
|
return getNodeOpacity(volumeActor, 1);
|
||||||
|
} else if (isVolumeWithConstantOpacity(volumeActor)) {
|
||||||
|
return getNodeOpacity(volumeActor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getWindowLevelsData = useCallback(
|
||||||
|
async (viewportId: number) => {
|
||||||
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||||
|
|
||||||
|
const volumeIds = viewport.getActors().map(actor => actor.uid);
|
||||||
|
const viewportProperties = viewport.getProperties();
|
||||||
|
const { voiRange } = viewportProperties;
|
||||||
|
const viewportVoi = voiRange
|
||||||
|
? {
|
||||||
|
windowWidth: voiRange.upper - voiRange.lower,
|
||||||
|
windowCenter: voiRange.lower + (voiRange.upper - voiRange.lower) / 2,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const windowLevels = await Promise.all(
|
||||||
|
volumeIds.map(async (volumeId, volumeIndex) => {
|
||||||
|
const volume = cs3DCache.getVolume(volumeId);
|
||||||
|
|
||||||
|
if (!volume) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const opacity = getVolumeOpacity(viewport, volumeId);
|
||||||
|
const { metadata, scaling } = volume;
|
||||||
|
const modality = metadata.Modality;
|
||||||
|
|
||||||
|
// TODO: find a proper way to fix the histogram
|
||||||
|
const options = {
|
||||||
|
min: modality === 'PT' ? 0.1 : -999,
|
||||||
|
max: modality === 'PT' ? 5 : 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const histogram =
|
||||||
|
cachedHistograms[volumeId] ??
|
||||||
|
(await getViewportVolumeHistogram(viewport, volume, options));
|
||||||
|
const { voi: displaySetVOI, colormap: displaySetColormap } =
|
||||||
|
viewportInfo.displaySetOptions[volumeIndex];
|
||||||
|
let colormap;
|
||||||
|
if (displaySetColormap) {
|
||||||
|
colormap =
|
||||||
|
csUtils.colormap.getColormap(displaySetColormap.name) ??
|
||||||
|
vtkColorMaps.getPresetByName(displaySetColormap.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const voi = !volumeIndex ? viewportVoi ?? displaySetVOI : displaySetVOI;
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewportId,
|
||||||
|
modality,
|
||||||
|
volumeId,
|
||||||
|
volumeIndex,
|
||||||
|
voi,
|
||||||
|
histogram,
|
||||||
|
colormap,
|
||||||
|
step: scaling?.PT ? 0.05 : 1,
|
||||||
|
opacity,
|
||||||
|
showOpacitySlider: volumeIndex === 1 && opacity !== undefined,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = windowLevels.filter(Boolean);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
[cachedHistograms, cornerstoneViewportService, getVolumeOpacity]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateViewportHistograms = useCallback(() => {
|
||||||
|
getWindowLevelsData(viewportId).then(windowLevels => {
|
||||||
|
setWindowLevels(windowLevels);
|
||||||
|
});
|
||||||
|
}, [viewportId, getWindowLevelsData]);
|
||||||
|
|
||||||
|
const handleCornerstoneVOIModified = useCallback(
|
||||||
|
e => {
|
||||||
|
const { detail } = e;
|
||||||
|
const { volumeId, range } = detail;
|
||||||
|
const oldWindowLevel = windowLevels.find(wl => wl.volumeId === volumeId);
|
||||||
|
|
||||||
|
if (!oldWindowLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldVOI = oldWindowLevel.voi;
|
||||||
|
const windowWidth = range.upper - range.lower;
|
||||||
|
const windowCenter = range.lower + windowWidth / 2;
|
||||||
|
|
||||||
|
if (windowWidth === oldVOI.windowWidth && windowCenter === oldVOI.windowCenter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWindowLevel = {
|
||||||
|
...oldWindowLevel,
|
||||||
|
voi: {
|
||||||
|
windowWidth,
|
||||||
|
windowCenter,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setWindowLevels(
|
||||||
|
windowLevels.map(windowLevel =>
|
||||||
|
windowLevel === oldWindowLevel ? newWindowLevel : windowLevel
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[windowLevels]
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedHandleCornerstoneVOIModified = useCallback(
|
||||||
|
debounce(handleCornerstoneVOIModified, 100),
|
||||||
|
[handleCornerstoneVOIModified]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVOIChange = useCallback(
|
||||||
|
(volumeId, voi) => {
|
||||||
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
const newRange = {
|
||||||
|
lower: voi.windowCenter - voi.windowWidth / 2,
|
||||||
|
upper: voi.windowCenter + voi.windowWidth / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
viewport.setProperties({ voiRange: newRange }, volumeId);
|
||||||
|
viewport.render();
|
||||||
|
},
|
||||||
|
[cornerstoneViewportService, viewportId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpacityChange = useCallback(
|
||||||
|
(viewportId, _volumeIndex, volumeId, opacity) => {
|
||||||
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
if (!viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportVolumeIds = viewport.getActors().map(actor => actor.uid);
|
||||||
|
const viewports = getViewportsWithVolumeIds(viewportVolumeIds);
|
||||||
|
|
||||||
|
viewports.forEach(vp => {
|
||||||
|
vp.setProperties({ colormap: { opacity } }, volumeId);
|
||||||
|
vp.render();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[getViewportsWithVolumeIds, cornerstoneViewportService]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen to windowLevels changes and caches all the new ones
|
||||||
|
useEffect(() => {
|
||||||
|
const newVolumeHistograms = windowLevels
|
||||||
|
.filter(windowLevel => !cachedHistograms[windowLevel.volumeId] && windowLevel.histogram)
|
||||||
|
.reduce((volumeHistograms, windowLevel) => {
|
||||||
|
// If the histogram exists, add it to the volumeHistograms object
|
||||||
|
if (windowLevel?.histogram) {
|
||||||
|
volumeHistograms[windowLevel.volumeId] = windowLevel.histogram;
|
||||||
|
}
|
||||||
|
return volumeHistograms;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
if (Object.keys(newVolumeHistograms).length) {
|
||||||
|
setCachedHistograms(prev => ({ ...prev, ...newVolumeHistograms }));
|
||||||
|
}
|
||||||
|
}, [windowLevels, cachedHistograms]);
|
||||||
|
|
||||||
|
// Updates the histogram when the viewport index prop has changed
|
||||||
|
useEffect(() => updateViewportHistograms(), [viewportId, updateViewportHistograms]);
|
||||||
|
|
||||||
|
// Listen to cornerstone events on "eventTarget" and at the document level
|
||||||
|
useEffect(() => {
|
||||||
|
eventTarget.addEventListener(Events.IMAGE_VOLUME_LOADING_COMPLETED, updateViewportHistograms);
|
||||||
|
|
||||||
|
document.addEventListener(Events.VOI_MODIFIED, debouncedHandleCornerstoneVOIModified, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventTarget.removeEventListener(
|
||||||
|
Events.IMAGE_VOLUME_LOADING_COMPLETED,
|
||||||
|
updateViewportHistograms
|
||||||
|
);
|
||||||
|
|
||||||
|
document.removeEventListener(
|
||||||
|
Events.VOI_MODIFIED,
|
||||||
|
debouncedHandleCornerstoneVOIModified,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [updateViewportHistograms, debouncedHandleCornerstoneVOIModified]);
|
||||||
|
|
||||||
|
// Updates the viewport when the context of the viewport has changed. This is
|
||||||
|
// necessary when moving across different stages because the viewport index
|
||||||
|
// may not change but the volumes loaded on it may change.
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||||
|
cornerstoneViewportService.EVENTS.VIEWPORT_VOLUMES_CHANGED,
|
||||||
|
({ viewportInfo }) => {
|
||||||
|
if (viewportInfo.viewportId === viewportId) {
|
||||||
|
updateViewportHistograms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [viewportId, cornerstoneViewportService, updateViewportHistograms]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelSection title="Window Level">
|
||||||
|
{windowLevels.map((windowLevel, i) => {
|
||||||
|
if (!windowLevel.histogram) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WindowLevel
|
||||||
|
key={windowLevel.volumeId}
|
||||||
|
title={`${windowLevel.modality}`}
|
||||||
|
histogram={windowLevel.histogram}
|
||||||
|
voi={windowLevel.voi}
|
||||||
|
step={windowLevel.step}
|
||||||
|
showOpacitySlider={windowLevel.showOpacitySlider}
|
||||||
|
colormap={windowLevel.colormap}
|
||||||
|
onVOIChange={voi => handleVOIChange(windowLevel.volumeId, voi)}
|
||||||
|
opacity={windowLevel.opacity}
|
||||||
|
onOpacityChange={opacity =>
|
||||||
|
handleOpacityChange(windowLevel.viewportId, i, windowLevel.volumeId, opacity)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PanelSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportWindowLevel.propTypes = {
|
||||||
|
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||||
|
viewportId: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportWindowLevel;
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { getWebWorkerManager } from '@cornerstonejs/core';
|
||||||
|
|
||||||
|
const workerManager = getWebWorkerManager();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
// maxWorkerInstances: 1,
|
||||||
|
// overwrite: false
|
||||||
|
autoTerminationOnIdle: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the task
|
||||||
|
const workerFn = () => {
|
||||||
|
return new Worker(new URL('./histogramWorker.js', import.meta.url), {
|
||||||
|
name: 'histogram-worker', // name used by the browser to name the worker
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
workerManager.registerWorker('histogram-worker', workerFn, options);
|
||||||
|
|
||||||
|
const getViewportVolumeHistogram = async (viewport, volume, options?) => {
|
||||||
|
if (!volume?.loadStatus.loaded) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const volumeImageData = viewport.getImageData(volume.volumeId);
|
||||||
|
|
||||||
|
if (!volumeImageData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scalarData = volume.scalarData;
|
||||||
|
|
||||||
|
let prevTimePoint;
|
||||||
|
if (volume.numTimePoints > 1) {
|
||||||
|
prevTimePoint = volume.timePointIndex;
|
||||||
|
const middleTimePoint = Math.round(volume.numTimePoints / 2);
|
||||||
|
volume.timePointIndex = middleTimePoint;
|
||||||
|
scalarData = volume.getScalarData(middleTimePoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dimensions, origin, direction, spacing } = volume;
|
||||||
|
|
||||||
|
const range = await workerManager.executeTask('histogram-worker', 'getRange', {
|
||||||
|
dimensions,
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
spacing,
|
||||||
|
scalarData,
|
||||||
|
});
|
||||||
|
|
||||||
|
// after we calculate the range let's reset the timePointIndex
|
||||||
|
if (volume.numTimePoints > 1) {
|
||||||
|
volume.timePointIndex = prevTimePoint;
|
||||||
|
}
|
||||||
|
const { minimum: min, maximum: max } = range;
|
||||||
|
const calcHistOptions = {
|
||||||
|
numBins: 256,
|
||||||
|
min: Math.max(min, options?.min ?? min),
|
||||||
|
max: Math.min(max, options?.max ?? max),
|
||||||
|
};
|
||||||
|
|
||||||
|
const histogram = await workerManager.executeTask('histogram-worker', 'calcHistogram', {
|
||||||
|
data: scalarData,
|
||||||
|
options: calcHistOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return histogram;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getViewportVolumeHistogram };
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
import { expose } from 'comlink';
|
||||||
|
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||||
|
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object simulates a heavy task by implementing a sleep function and a recursive Fibonacci function.
|
||||||
|
* It's used for testing or demonstrating purposes where a heavy or time-consuming task is needed.
|
||||||
|
*/
|
||||||
|
const obj = {
|
||||||
|
getRange: ({ dimensions, origin, direction, spacing, scalarData }) => {
|
||||||
|
const imageData = vtkImageData.newInstance();
|
||||||
|
imageData.setDimensions(dimensions);
|
||||||
|
imageData.setOrigin(origin);
|
||||||
|
imageData.setDirection(direction);
|
||||||
|
imageData.setSpacing(spacing);
|
||||||
|
|
||||||
|
const scalarArray = vtkDataArray.newInstance({
|
||||||
|
name: 'Pixels',
|
||||||
|
numberOfComponents: 1,
|
||||||
|
values: scalarData,
|
||||||
|
});
|
||||||
|
|
||||||
|
imageData.getPointData().setScalars(scalarArray);
|
||||||
|
|
||||||
|
imageData.modified();
|
||||||
|
|
||||||
|
const range = imageData.computeHistogram(imageData.getBounds());
|
||||||
|
|
||||||
|
return range;
|
||||||
|
},
|
||||||
|
calcHistogram: ({ data, options }) => {
|
||||||
|
if (options === undefined) {
|
||||||
|
options = {};
|
||||||
|
}
|
||||||
|
const histogram = {
|
||||||
|
numBins: options.numBins || 256,
|
||||||
|
range: { min: 0, max: 0 },
|
||||||
|
bins: new Int32Array(1),
|
||||||
|
maxBin: 0,
|
||||||
|
maxBinValue: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let minToUse = options.min;
|
||||||
|
let maxToUse = options.max;
|
||||||
|
|
||||||
|
if (minToUse === undefined || maxToUse === undefined) {
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
let index = data.length;
|
||||||
|
|
||||||
|
while (index--) {
|
||||||
|
const value = data[index];
|
||||||
|
if (value < min) {
|
||||||
|
min = value;
|
||||||
|
}
|
||||||
|
if (value > max) {
|
||||||
|
max = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minToUse = min;
|
||||||
|
maxToUse = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
histogram.range = { min: minToUse, max: maxToUse };
|
||||||
|
|
||||||
|
const bins = new Int32Array(histogram.numBins);
|
||||||
|
const binScale = histogram.numBins / (maxToUse - minToUse);
|
||||||
|
|
||||||
|
for (let index = 0; index < data.length; index++) {
|
||||||
|
const value = data[index];
|
||||||
|
if (value < minToUse) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (value > maxToUse) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bin = Math.floor((value - minToUse) * binScale);
|
||||||
|
bins[bin] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
histogram.bins = bins;
|
||||||
|
histogram.maxBin = 0;
|
||||||
|
histogram.maxBinValue = 0;
|
||||||
|
|
||||||
|
for (let bin = 0; bin < histogram.numBins; bin++) {
|
||||||
|
if (histogram.bins[bin] > histogram.maxBinValue) {
|
||||||
|
histogram.maxBin = bin;
|
||||||
|
histogram.maxBinValue = histogram.bins[bin];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return histogram;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expose(obj);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ViewportWindowLevel';
|
||||||
@ -192,7 +192,7 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
const displaySets = displaySetUIDs.map(displaySetService.getDisplaySetByUID);
|
||||||
const isUS = displaySets.some(displaySet => displaySet.Modality === 'US');
|
const isUS = displaySets.some(displaySet => displaySet?.Modality === 'US');
|
||||||
if (!isUS) {
|
if (!isUS) {
|
||||||
return {
|
return {
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
imageLoadPoolManager,
|
imageLoadPoolManager,
|
||||||
imageRetrievalPoolManager,
|
imageRetrievalPoolManager,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
|
import * as csStreamingImageVolumeLoader from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
import { ServicesManager, Types } from '@ohif/core';
|
import { ServicesManager, Types } from '@ohif/core';
|
||||||
|
|
||||||
@ -34,6 +35,10 @@ import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
|||||||
import { showLabelAnnotationPopup } from './utils/callInputDialog';
|
import { showLabelAnnotationPopup } from './utils/callInputDialog';
|
||||||
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
import ViewportActionCornersService from './services/ViewportActionCornersService/ViewportActionCornersService';
|
||||||
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
import { ViewportActionCornersProvider } from './contextProviders/ViewportActionCornersProvider';
|
||||||
|
import ActiveViewportWindowLevel from './components/ActiveViewportWindowLevel';
|
||||||
|
|
||||||
|
const { helpers: volumeLoaderHelpers } = csStreamingImageVolumeLoader;
|
||||||
|
const { getDynamicVolumeInfo } = volumeLoaderHelpers ?? {};
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
||||||
@ -90,6 +95,16 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getToolbarModule,
|
getToolbarModule,
|
||||||
|
getPanelModule({ servicesManager }) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'activeViewportWindowLevel',
|
||||||
|
component: () => {
|
||||||
|
return <ActiveViewportWindowLevel servicesManager={servicesManager} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
getViewportModule({ servicesManager, commandsManager }) {
|
getViewportModule({ servicesManager, commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
@ -127,7 +142,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
},
|
},
|
||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
dicomLoaderService,
|
dicomLoaderService,
|
||||||
showLabelAnnotationPopup
|
showLabelAnnotationPopup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -143,6 +158,12 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
|||||||
Enums: cs3DToolsEnums,
|
Enums: cs3DToolsEnums,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'volumeLoader',
|
||||||
|
exports: {
|
||||||
|
getDynamicVolumeInfo,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,11 @@ import {
|
|||||||
utilities as csUtilities,
|
utilities as csUtilities,
|
||||||
Enums as csEnums,
|
Enums as csEnums,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
import {
|
||||||
|
cornerstoneStreamingImageVolumeLoader,
|
||||||
|
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||||
|
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
|
|
||||||
import initWADOImageLoader from './initWADOImageLoader';
|
import initWADOImageLoader from './initWADOImageLoader';
|
||||||
import initCornerstoneTools from './initCornerstoneTools';
|
import initCornerstoneTools from './initCornerstoneTools';
|
||||||
@ -29,10 +33,9 @@ import initContextMenu from './initContextMenu';
|
|||||||
import initDoubleClick from './initDoubleClick';
|
import initDoubleClick from './initDoubleClick';
|
||||||
import { CornerstoneServices } from './types';
|
import { CornerstoneServices } from './types';
|
||||||
import initViewTiming from './utils/initViewTiming';
|
import initViewTiming from './utils/initViewTiming';
|
||||||
import { utilities } from '@cornerstonejs/core';
|
|
||||||
import { colormaps } from './utils/colormaps';
|
import { colormaps } from './utils/colormaps';
|
||||||
|
|
||||||
const { registerColormap } = utilities.colormap;
|
const { registerColormap } = csUtilities.colormap;
|
||||||
|
|
||||||
// TODO: Cypress tests are currently grabbing this from the window?
|
// TODO: Cypress tests are currently grabbing this from the window?
|
||||||
window.cornerstone = cornerstone;
|
window.cornerstone = cornerstone;
|
||||||
@ -93,7 +96,6 @@ export default async function init({
|
|||||||
customizationService,
|
customizationService,
|
||||||
uiModalService,
|
uiModalService,
|
||||||
uiNotificationService,
|
uiNotificationService,
|
||||||
cineService,
|
|
||||||
cornerstoneViewportService,
|
cornerstoneViewportService,
|
||||||
hangingProtocolService,
|
hangingProtocolService,
|
||||||
toolbarService,
|
toolbarService,
|
||||||
@ -172,6 +174,11 @@ export default async function init({
|
|||||||
cornerstoneStreamingImageVolumeLoader
|
cornerstoneStreamingImageVolumeLoader
|
||||||
);
|
);
|
||||||
|
|
||||||
|
volumeLoader.registerVolumeLoader(
|
||||||
|
'cornerstoneStreamingDynamicImageVolume',
|
||||||
|
cornerstoneStreamingDynamicImageVolumeLoader
|
||||||
|
);
|
||||||
|
|
||||||
hangingProtocolService.registerImageLoadStrategy('interleaveCenter', interleaveCenterLoader);
|
hangingProtocolService.registerImageLoadStrategy('interleaveCenter', interleaveCenterLoader);
|
||||||
hangingProtocolService.registerImageLoadStrategy('interleaveTopToBottom', interleaveTopToBottom);
|
hangingProtocolService.registerImageLoadStrategy('interleaveTopToBottom', interleaveTopToBottom);
|
||||||
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
|
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
|
||||||
@ -195,7 +202,7 @@ export default async function init({
|
|||||||
/* Measurement Service */
|
/* Measurement Service */
|
||||||
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
|
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
|
||||||
|
|
||||||
initCineService(cineService);
|
initCineService(servicesManager);
|
||||||
|
|
||||||
// When a custom image load is performed, update the relevant viewports
|
// When a custom image load is performed, update the relevant viewports
|
||||||
hangingProtocolService.subscribe(
|
hangingProtocolService.subscribe(
|
||||||
|
|||||||
@ -1,6 +1,60 @@
|
|||||||
|
import { cache } from '@cornerstonejs/core';
|
||||||
import { utilities } from '@cornerstonejs/tools';
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
|
||||||
function initCineService(cineService) {
|
function _getVolumesFromViewport(viewport) {
|
||||||
|
return viewport ? viewport.getActors().map(actor => cache.getVolume(actor.uid)) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getVolumeFromViewport(viewport) {
|
||||||
|
const volumes = _getVolumesFromViewport(viewport);
|
||||||
|
const dynamicVolume = volumes.find(volume => volume.isDynamicVolume());
|
||||||
|
|
||||||
|
return dynamicVolume ?? volumes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all viewports that needs to be synchronized with the source
|
||||||
|
* viewport passed as parameter when cine is updated.
|
||||||
|
* @param servicesManager ServiceManager
|
||||||
|
* @param srcViewportIndex Source viewport index
|
||||||
|
* @returns array with viewport information.
|
||||||
|
*/
|
||||||
|
function _getSyncedViewports(servicesManager, srcViewportId) {
|
||||||
|
const { viewportGridService, cornerstoneViewportService } = servicesManager.services;
|
||||||
|
|
||||||
|
const { viewports: viewportsStates } = viewportGridService.getState();
|
||||||
|
const srcViewportState = viewportsStates.get(srcViewportId);
|
||||||
|
|
||||||
|
if (srcViewportState?.viewportOptions?.viewportType !== 'volume') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcViewport = cornerstoneViewportService.getCornerstoneViewport(srcViewportId);
|
||||||
|
|
||||||
|
const srcVolume = srcViewport ? _getVolumeFromViewport(srcViewport) : null;
|
||||||
|
|
||||||
|
if (!srcVolume?.isDynamicVolume()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { volumeId: srcVolumeId } = srcVolume;
|
||||||
|
|
||||||
|
return Array.from(viewportsStates.values())
|
||||||
|
.filter(({ viewportId }) => {
|
||||||
|
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||||
|
|
||||||
|
return viewportId !== srcViewportId && viewport?.hasVolumeId(srcVolumeId);
|
||||||
|
})
|
||||||
|
.map(({ viewportId }) => ({ viewportId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCineService(servicesManager) {
|
||||||
|
const { cineService } = servicesManager.services;
|
||||||
|
|
||||||
|
const getSyncedViewports = viewportId => {
|
||||||
|
return _getSyncedViewports(servicesManager, viewportId);
|
||||||
|
};
|
||||||
|
|
||||||
const playClip = (element, playClipOptions) => {
|
const playClip = (element, playClipOptions) => {
|
||||||
return utilities.cine.playClip(element, playClipOptions);
|
return utilities.cine.playClip(element, playClipOptions);
|
||||||
};
|
};
|
||||||
@ -9,7 +63,11 @@ function initCineService(cineService) {
|
|||||||
return utilities.cine.stopClip(element);
|
return utilities.cine.stopClip(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
cineService.setServiceImplementation({ playClip, stopClip });
|
cineService.setServiceImplementation({
|
||||||
|
getSyncedViewports,
|
||||||
|
playClip,
|
||||||
|
stopClip,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default initCineService;
|
export default initCineService;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
MIPJumpToClickTool,
|
MIPJumpToClickTool,
|
||||||
LengthTool,
|
LengthTool,
|
||||||
RectangleROITool,
|
RectangleROITool,
|
||||||
|
RectangleROIThresholdTool,
|
||||||
EllipticalROITool,
|
EllipticalROITool,
|
||||||
CircleROITool,
|
CircleROITool,
|
||||||
BidirectionalTool,
|
BidirectionalTool,
|
||||||
@ -19,14 +20,16 @@ import {
|
|||||||
MagnifyTool,
|
MagnifyTool,
|
||||||
CrosshairsTool,
|
CrosshairsTool,
|
||||||
SegmentationDisplayTool,
|
SegmentationDisplayTool,
|
||||||
|
RectangleScissorsTool,
|
||||||
|
SphereScissorsTool,
|
||||||
|
CircleScissorsTool,
|
||||||
|
BrushTool,
|
||||||
|
PaintFillTool,
|
||||||
init,
|
init,
|
||||||
addTool,
|
addTool,
|
||||||
annotation,
|
annotation,
|
||||||
ReferenceLinesTool,
|
ReferenceLinesTool,
|
||||||
TrackballRotateTool,
|
TrackballRotateTool,
|
||||||
CircleScissorsTool,
|
|
||||||
RectangleScissorsTool,
|
|
||||||
SphereScissorsTool,
|
|
||||||
AdvancedMagnifyTool,
|
AdvancedMagnifyTool,
|
||||||
UltrasoundDirectionalTool,
|
UltrasoundDirectionalTool,
|
||||||
PlanarFreehandROITool,
|
PlanarFreehandROITool,
|
||||||
@ -52,6 +55,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(MIPJumpToClickTool);
|
addTool(MIPJumpToClickTool);
|
||||||
addTool(LengthTool);
|
addTool(LengthTool);
|
||||||
addTool(RectangleROITool);
|
addTool(RectangleROITool);
|
||||||
|
addTool(RectangleROIThresholdTool);
|
||||||
addTool(EllipticalROITool);
|
addTool(EllipticalROITool);
|
||||||
addTool(CircleROITool);
|
addTool(CircleROITool);
|
||||||
addTool(BidirectionalTool);
|
addTool(BidirectionalTool);
|
||||||
@ -62,12 +66,14 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(MagnifyTool);
|
addTool(MagnifyTool);
|
||||||
addTool(CrosshairsTool);
|
addTool(CrosshairsTool);
|
||||||
addTool(SegmentationDisplayTool);
|
addTool(SegmentationDisplayTool);
|
||||||
|
addTool(RectangleScissorsTool);
|
||||||
|
addTool(SphereScissorsTool);
|
||||||
|
addTool(CircleScissorsTool);
|
||||||
|
addTool(BrushTool);
|
||||||
|
addTool(PaintFillTool);
|
||||||
addTool(ReferenceLinesTool);
|
addTool(ReferenceLinesTool);
|
||||||
addTool(CalibrationLineTool);
|
addTool(CalibrationLineTool);
|
||||||
addTool(TrackballRotateTool);
|
addTool(TrackballRotateTool);
|
||||||
addTool(CircleScissorsTool);
|
|
||||||
addTool(RectangleScissorsTool);
|
|
||||||
addTool(SphereScissorsTool);
|
|
||||||
addTool(ImageOverlayViewerTool);
|
addTool(ImageOverlayViewerTool);
|
||||||
addTool(AdvancedMagnifyTool);
|
addTool(AdvancedMagnifyTool);
|
||||||
addTool(UltrasoundDirectionalTool);
|
addTool(UltrasoundDirectionalTool);
|
||||||
@ -103,6 +109,7 @@ const toolNames = {
|
|||||||
DragProbe: DragProbeTool.toolName,
|
DragProbe: DragProbeTool.toolName,
|
||||||
Probe: ProbeTool.toolName,
|
Probe: ProbeTool.toolName,
|
||||||
RectangleROI: RectangleROITool.toolName,
|
RectangleROI: RectangleROITool.toolName,
|
||||||
|
RectangleROIThreshold: RectangleROIThresholdTool.toolName,
|
||||||
EllipticalROI: EllipticalROITool.toolName,
|
EllipticalROI: EllipticalROITool.toolName,
|
||||||
CircleROI: CircleROITool.toolName,
|
CircleROI: CircleROITool.toolName,
|
||||||
Bidirectional: BidirectionalTool.toolName,
|
Bidirectional: BidirectionalTool.toolName,
|
||||||
@ -111,6 +118,8 @@ const toolNames = {
|
|||||||
Magnify: MagnifyTool.toolName,
|
Magnify: MagnifyTool.toolName,
|
||||||
Crosshairs: CrosshairsTool.toolName,
|
Crosshairs: CrosshairsTool.toolName,
|
||||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||||
|
Brush: BrushTool.toolName,
|
||||||
|
PaintFill: PaintFillTool.toolName,
|
||||||
ReferenceLines: ReferenceLinesTool.toolName,
|
ReferenceLines: ReferenceLinesTool.toolName,
|
||||||
CalibrationLine: CalibrationLineTool.toolName,
|
CalibrationLine: CalibrationLineTool.toolName,
|
||||||
TrackballRotateTool: TrackballRotateTool.toolName,
|
TrackballRotateTool: TrackballRotateTool.toolName,
|
||||||
|
|||||||
@ -49,6 +49,20 @@ const initMeasurementService = (
|
|||||||
Length.toMeasurement
|
Length.toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
measurementService.addMapping(
|
||||||
|
csTools3DVer1MeasurementSource,
|
||||||
|
'Crosshairs',
|
||||||
|
Length.matchingCriteria,
|
||||||
|
() => {
|
||||||
|
console.warn('Crosshairs mapping not implemented.');
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
console.warn('Crosshairs mapping not implemented.');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
measurementService.addMapping(
|
measurementService.addMapping(
|
||||||
csTools3DVer1MeasurementSource,
|
csTools3DVer1MeasurementSource,
|
||||||
'Bidirectional',
|
'Bidirectional',
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
import { volumeLoader } from '@cornerstonejs/core';
|
import { volumeLoader } from '@cornerstonejs/core';
|
||||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
import {
|
||||||
|
cornerstoneStreamingImageVolumeLoader,
|
||||||
|
cornerstoneStreamingDynamicImageVolumeLoader,
|
||||||
|
} from '@cornerstonejs/streaming-image-volume-loader';
|
||||||
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
||||||
import dicomParser from 'dicom-parser';
|
import dicomParser from 'dicom-parser';
|
||||||
import { errorHandler, utils } from '@ohif/core';
|
import { errorHandler, utils } from '@ohif/core';
|
||||||
@ -41,6 +44,11 @@ export default function initWADOImageLoader(
|
|||||||
|
|
||||||
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
||||||
|
|
||||||
|
registerVolumeLoader(
|
||||||
|
'cornerstoneStreamingDynamicImageVolume',
|
||||||
|
cornerstoneStreamingDynamicImageVolumeLoader
|
||||||
|
);
|
||||||
|
|
||||||
dicomImageLoader.configure({
|
dicomImageLoader.configure({
|
||||||
decodeConfig: {
|
decodeConfig: {
|
||||||
// !! IMPORTANT !!
|
// !! IMPORTANT !!
|
||||||
|
|||||||
@ -200,6 +200,7 @@ class CornerstoneCacheService {
|
|||||||
volume,
|
volume,
|
||||||
volumeId,
|
volumeId,
|
||||||
imageIds: volumeImageIds,
|
imageIds: volumeImageIds,
|
||||||
|
isDynamicVolume: displaySet.isDynamicVolume,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +229,7 @@ class CornerstoneCacheService {
|
|||||||
|
|
||||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||||
viewportDisplaySetInstanceUIDs,
|
viewportDisplaySetInstanceUIDs,
|
||||||
instance.FrameOfReferenceUID
|
instance?.FrameOfReferenceUID || segDisplaySet.FrameOfReferenceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldDisplaySeg) {
|
if (shouldDisplaySeg) {
|
||||||
|
|||||||
@ -1289,7 +1289,7 @@ class SegmentationService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { colorLUTIndex } = segmentation;
|
const { colorLUTIndex } = segmentation;
|
||||||
this._removeSegmentationFromCornerstone(segmentationId);
|
const { updatedToolGroupIds } = this._removeSegmentationFromCornerstone(segmentationId);
|
||||||
|
|
||||||
// Delete associated colormap
|
// Delete associated colormap
|
||||||
// Todo: bring this back
|
// Todo: bring this back
|
||||||
@ -1309,7 +1309,9 @@ class SegmentationService extends PubSubService {
|
|||||||
if (remainingHydratedSegmentations.length) {
|
if (remainingHydratedSegmentations.length) {
|
||||||
const { id } = remainingHydratedSegmentations[0];
|
const { id } = remainingHydratedSegmentations[0];
|
||||||
|
|
||||||
this._setActiveSegmentationForToolGroup(id, this._getApplicableToolGroupId(), false);
|
updatedToolGroupIds.forEach(toolGroupId => {
|
||||||
|
this._setActiveSegmentationForToolGroup(id, toolGroupId, false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1966,6 +1968,7 @@ class SegmentationService extends PubSubService {
|
|||||||
const removeFromCache = true;
|
const removeFromCache = true;
|
||||||
const segmentationState = cstSegmentation.state;
|
const segmentationState = cstSegmentation.state;
|
||||||
const sourceSegState = segmentationState.getSegmentation(segmentationId);
|
const sourceSegState = segmentationState.getSegmentation(segmentationId);
|
||||||
|
const updatedToolGroupIds: Set<string> = new Set();
|
||||||
|
|
||||||
if (!sourceSegState) {
|
if (!sourceSegState) {
|
||||||
return;
|
return;
|
||||||
@ -1981,6 +1984,7 @@ class SegmentationService extends PubSubService {
|
|||||||
segmentationRepresentations.forEach(representation => {
|
segmentationRepresentations.forEach(representation => {
|
||||||
if (representation.segmentationId === segmentationId) {
|
if (representation.segmentationId === segmentationId) {
|
||||||
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
||||||
|
updatedToolGroupIds.add(toolGroupId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1998,6 +2002,8 @@ class SegmentationService extends PubSubService {
|
|||||||
if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) {
|
if (removeFromCache && cache.getVolumeLoadObject(segmentationId)) {
|
||||||
cache.removeVolumeLoadObject(segmentationId);
|
cache.removeVolumeLoadObject(segmentationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { updatedToolGroupIds: Array.from(updatedToolGroupIds) };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) {
|
private _updateCornerstoneSegmentations({ segmentationId, notYetUpdatedAtSource }) {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
import { ToolGroupManager, Enums, Types } from '@cornerstonejs/tools';
|
||||||
|
import { eventTarget } from '@cornerstonejs/core';
|
||||||
|
|
||||||
import { Types as OhifTypes, pubSubServiceInterface } from '@ohif/core';
|
import { Types as OhifTypes, pubSubServiceInterface } from '@ohif/core';
|
||||||
import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnabledElement';
|
import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnabledElement';
|
||||||
@ -6,6 +7,8 @@ import getActiveViewportEnabledElement from '../../utils/getActiveViewportEnable
|
|||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
VIEWPORT_ADDED: 'event::cornerstone::toolgroupservice:viewportadded',
|
||||||
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
|
TOOLGROUP_CREATED: 'event::cornerstone::toolgroupservice:toolgroupcreated',
|
||||||
|
TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:toolactivated',
|
||||||
|
PRIMARY_TOOL_ACTIVATED: 'event::cornerstone::toolgroupservice:primarytoolactivated',
|
||||||
};
|
};
|
||||||
|
|
||||||
type Tool = {
|
type Tool = {
|
||||||
@ -38,18 +41,26 @@ export default class ToolGroupService {
|
|||||||
EVENTS: { [key: string]: string };
|
EVENTS: { [key: string]: string };
|
||||||
|
|
||||||
constructor(serviceManager) {
|
constructor(serviceManager) {
|
||||||
const { cornerstoneViewportService, viewportGridService } = serviceManager.services;
|
const { cornerstoneViewportService, viewportGridService, uiNotificationService } =
|
||||||
|
serviceManager.services;
|
||||||
this.cornerstoneViewportService = cornerstoneViewportService;
|
this.cornerstoneViewportService = cornerstoneViewportService;
|
||||||
this.viewportGridService = viewportGridService;
|
this.viewportGridService = viewportGridService;
|
||||||
|
this.uiNotificationService = uiNotificationService;
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
|
|
||||||
|
this._init();
|
||||||
}
|
}
|
||||||
|
|
||||||
onModeExit() {
|
onModeExit() {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _init() {
|
||||||
|
eventTarget.addEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a tool group from the ToolGroupManager by tool group ID.
|
* Retrieves a tool group from the ToolGroupManager by tool group ID.
|
||||||
* If no tool group ID is provided, it retrieves the tool group of the active viewport.
|
* If no tool group ID is provided, it retrieves the tool group of the active viewport.
|
||||||
@ -105,12 +116,14 @@ export default class ToolGroupService {
|
|||||||
return toolGroup.getActivePrimaryMouseButtonTool();
|
return toolGroup.getActivePrimaryMouseButtonTool();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy(): void {
|
||||||
ToolGroupManager.destroy();
|
ToolGroupManager.destroy();
|
||||||
this.toolGroupIds = new Set();
|
this.toolGroupIds = new Set();
|
||||||
|
|
||||||
|
eventTarget.removeEventListener(Enums.Events.TOOL_ACTIVATED, this._onToolActivated);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroyToolGroup(toolGroupId: string) {
|
public destroyToolGroup(toolGroupId: string): void {
|
||||||
ToolGroupManager.destroyToolGroup(toolGroupId);
|
ToolGroupManager.destroyToolGroup(toolGroupId);
|
||||||
this.toolGroupIds.delete(toolGroupId);
|
this.toolGroupIds.delete(toolGroupId);
|
||||||
}
|
}
|
||||||
@ -222,6 +235,10 @@ export default class ToolGroupService {
|
|||||||
toolInstance.configuration = config;
|
toolInstance.configuration = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getActivePrimaryMouseButtonTool(toolGroupId?: string): string {
|
||||||
|
return this.getToolGroup(toolGroupId)?.getActivePrimaryMouseButtonTool();
|
||||||
|
}
|
||||||
|
|
||||||
private _setToolsMode(toolGroup, tools) {
|
private _setToolsMode(toolGroup, tools) {
|
||||||
const { active, passive, enabled, disabled } = tools;
|
const { active, passive, enabled, disabled } = tools;
|
||||||
|
|
||||||
@ -279,4 +296,23 @@ export default class ToolGroupService {
|
|||||||
addTools(tools.disabled);
|
addTools(tools.disabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _onToolActivated = (evt: Types.EventTypes.ToolActivatedEventType) => {
|
||||||
|
const { toolGroupId, toolName, toolBindingsOptions } = evt.detail;
|
||||||
|
const isPrimaryTool = toolBindingsOptions.bindings?.some(
|
||||||
|
binding => binding.mouseButton === Enums.MouseBindings.Primary
|
||||||
|
);
|
||||||
|
|
||||||
|
const callbackProps = {
|
||||||
|
toolGroupId,
|
||||||
|
toolName,
|
||||||
|
toolBindingsOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._broadcastEvent(EVENTS.TOOL_ACTIVATED, callbackProps);
|
||||||
|
|
||||||
|
if (isPrimaryTool) {
|
||||||
|
this._broadcastEvent(EVENTS.PRIMARY_TOOL_ACTIVATED, callbackProps);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import JumpPresets from '../../utils/JumpPresets';
|
|||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
VIEWPORT_DATA_CHANGED: 'event::cornerstoneViewportService:viewportDataChanged',
|
||||||
|
VIEWPORT_VOLUMES_CHANGED: 'event::cornerstoneViewportService:viewportVolumesChanged',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -793,6 +794,10 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewport.render();
|
viewport.render();
|
||||||
|
|
||||||
|
this._broadcastEvent(this.EVENTS.VIEWPORT_VOLUMES_CHANGED, {
|
||||||
|
viewportInfo,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
private _addSegmentationRepresentationToToolGroupIfNecessary(
|
||||||
|
|||||||
@ -37,6 +37,31 @@ export default function interleaveTopToBottom({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredMatchDetails = [];
|
||||||
|
const displaySetsToLoad = new Set();
|
||||||
|
|
||||||
|
// Check all viewports that have a displaySet to be loaded. In some cases
|
||||||
|
// (eg: line chart viewports which is not a Cornerstone viewport) the
|
||||||
|
// displaySet is created on the client and there are no instances to be
|
||||||
|
// downloaded. For those viewports the displaySet may have the `skipLoading`
|
||||||
|
// option set to true otherwise it may block the download of all other
|
||||||
|
// instances resulting in blank viewports.
|
||||||
|
Array.from(matchDetails.values()).forEach(curMatchDetails => {
|
||||||
|
const { displaySetsInfo } = curMatchDetails;
|
||||||
|
let numDisplaySetsToLoad = 0;
|
||||||
|
|
||||||
|
displaySetsInfo.forEach(({ displaySetInstanceUID, displaySetOptions }) => {
|
||||||
|
if (!displaySetOptions?.options?.skipLoading) {
|
||||||
|
numDisplaySetsToLoad++;
|
||||||
|
displaySetsToLoad.add(displaySetInstanceUID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (numDisplaySetsToLoad) {
|
||||||
|
filteredMatchDetails.push(curMatchDetails);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The following is checking if all the viewports that were matched in the HP has been
|
* 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
|
* successfully created their cornerstone viewport or not. Todo: This can be
|
||||||
@ -50,16 +75,19 @@ export default function interleaveTopToBottom({
|
|||||||
* listen to it and as the other viewports are created we can set the volumes for them
|
* listen to it and as the other viewports are created we can set the volumes for them
|
||||||
* since volumes are already started loading.
|
* since volumes are already started loading.
|
||||||
*/
|
*/
|
||||||
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
if (filteredMatchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if all the matched volumes are loaded
|
// Check if all the matched volumes are loaded
|
||||||
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
for (const [_, details] of displaySetsMatchDetails.entries()) {
|
||||||
const { SeriesInstanceUID } = details;
|
const { SeriesInstanceUID, displaySetInstanceUID } = details;
|
||||||
|
|
||||||
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
// HangingProtocol has matched, but don't have all the volumes created yet, so return
|
||||||
if (!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)) {
|
if (
|
||||||
|
displaySetsToLoad.has(displaySetInstanceUID) &&
|
||||||
|
!Array.from(volumeIdMapsToLoad.values()).includes(SeriesInstanceUID)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LineChart } from '@ohif/ui';
|
||||||
|
|
||||||
|
const LineChartViewport = ({ displaySets }) => {
|
||||||
|
const displaySet = displaySets[0];
|
||||||
|
const { axis: chartAxis, series: chartSeries } = displaySet.instance.chartData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LineChart
|
||||||
|
showLegend={true}
|
||||||
|
legendWidth={150}
|
||||||
|
axis={{
|
||||||
|
x: {
|
||||||
|
label: chartAxis.x.label,
|
||||||
|
indexRef: 0,
|
||||||
|
type: 'x',
|
||||||
|
range: {
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
label: chartAxis.y.label,
|
||||||
|
indexRef: 1,
|
||||||
|
type: 'y',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
series={chartSeries}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { LineChartViewport as default };
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './LineChartViewport';
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { SidePanel } from '@ohif/ui';
|
import { SidePanel } from '@ohif/ui';
|
||||||
import { PanelService, ServicesManager } from '@ohif/core';
|
import { PanelService, ServicesManager, Types } from '@ohif/core';
|
||||||
|
|
||||||
export type SidePanelWithServicesProps = {
|
export type SidePanelWithServicesProps = {
|
||||||
servicesManager: ServicesManager;
|
servicesManager: ServicesManager;
|
||||||
@ -14,10 +14,10 @@ export type SidePanelWithServicesProps = {
|
|||||||
const SidePanelWithServices = ({
|
const SidePanelWithServices = ({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
side,
|
side,
|
||||||
className,
|
|
||||||
activeTabIndex: activeTabIndexProp,
|
activeTabIndex: activeTabIndexProp,
|
||||||
tabs,
|
tabs: tabsProp,
|
||||||
expandedWidth,
|
expandedWidth,
|
||||||
|
...props
|
||||||
}: SidePanelWithServicesProps) => {
|
}: SidePanelWithServicesProps) => {
|
||||||
const panelService: PanelService = servicesManager?.services?.panelService;
|
const panelService: PanelService = servicesManager?.services?.panelService;
|
||||||
|
|
||||||
@ -25,36 +25,64 @@ const SidePanelWithServices = ({
|
|||||||
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||||
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
||||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
||||||
|
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
|
||||||
|
|
||||||
|
const handleSidePanelOpen = useCallback(() => {
|
||||||
|
setHasBeenOpened(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
|
||||||
|
setActiveTabIndex(activeTabIndex);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/** update the active tab index from outside */
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveTabIndex(activeTabIndexProp);
|
||||||
|
}, [activeTabIndexProp]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (panelService) {
|
const { unsubscribe } = panelService.subscribe(
|
||||||
const activatePanelSubscription = panelService.subscribe(
|
panelService.EVENTS.PANELS_CHANGED,
|
||||||
panelService.EVENTS.ACTIVATE_PANEL,
|
panelChangedEvent => {
|
||||||
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
if (panelChangedEvent.position !== side) {
|
||||||
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
return;
|
||||||
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
}
|
||||||
if (tabIndex !== -1) {
|
|
||||||
setActiveTabIndex(tabIndex);
|
setTabs(panelService.getPanels(side));
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [panelService, side]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const activatePanelSubscription = panelService.subscribe(
|
||||||
|
panelService.EVENTS.ACTIVATE_PANEL,
|
||||||
|
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||||
|
if (!hasBeenOpened || activatePanelEvent.forceActive) {
|
||||||
|
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
||||||
|
if (tabIndex !== -1) {
|
||||||
|
setActiveTabIndex(tabIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
activatePanelSubscription.unsubscribe();
|
activatePanelSubscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}, [tabs, hasBeenOpened, panelService]);
|
}, [tabs, hasBeenOpened, panelService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidePanel
|
<SidePanel
|
||||||
|
{...props}
|
||||||
side={side}
|
side={side}
|
||||||
className={className}
|
|
||||||
activeTabIndex={activeTabIndex}
|
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
onOpen={() => {
|
activeTabIndex={activeTabIndex}
|
||||||
setHasBeenOpened(true);
|
onOpen={handleSidePanelOpen}
|
||||||
}}
|
onActiveTabIndexChange={handleActiveTabIndexChange}
|
||||||
expandedWidth={expandedWidth}
|
expandedWidth={expandedWidth}
|
||||||
></SidePanel>
|
></SidePanel>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { LegacyButton, LegacyButtonGroup } from '@ohif/ui';
|
|
||||||
|
|
||||||
function ActionButtons({ onExportClick, onCreateReportClick }) {
|
|
||||||
const { t } = useTranslation('MeasurementTable');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<LegacyButtonGroup
|
|
||||||
color="black"
|
|
||||||
size="inherit"
|
|
||||||
>
|
|
||||||
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
|
|
||||||
<LegacyButton
|
|
||||||
className="px-2 py-2 text-base"
|
|
||||||
onClick={onExportClick}
|
|
||||||
>
|
|
||||||
{t('Export CSV')}
|
|
||||||
</LegacyButton>
|
|
||||||
<LegacyButton
|
|
||||||
className="px-2 py-2 text-base"
|
|
||||||
onClick={onCreateReportClick}
|
|
||||||
>
|
|
||||||
{t('Create Report')}
|
|
||||||
</LegacyButton>
|
|
||||||
</LegacyButtonGroup>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionButtons.propTypes = {
|
|
||||||
onExportClick: PropTypes.func,
|
|
||||||
onCreateReportClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
ActionButtons.defaultProps = {
|
|
||||||
onExportClick: () => alert('Export'),
|
|
||||||
onCreateReportClick: () => alert('Create Report'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionButtons;
|
|
||||||
@ -2,8 +2,14 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { utils, ServicesManager } from '@ohif/core';
|
import { utils, ServicesManager } from '@ohif/core';
|
||||||
import { MeasurementTable, Dialog, Input, useViewportGrid, ButtonEnums } from '@ohif/ui';
|
import {
|
||||||
import ActionButtons from './ActionButtons';
|
MeasurementTable,
|
||||||
|
Dialog,
|
||||||
|
Input,
|
||||||
|
useViewportGrid,
|
||||||
|
ButtonEnums,
|
||||||
|
ActionButtons,
|
||||||
|
} from '@ohif/ui';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
import createReportDialogPrompt, {
|
import createReportDialogPrompt, {
|
||||||
@ -212,7 +218,7 @@ export default function PanelMeasurementTable({
|
|||||||
data-cy={'measurements-panel'}
|
data-cy={'measurements-panel'}
|
||||||
>
|
>
|
||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
title={t("Measurements")}
|
title={t('Measurements')}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
data={displayMeasurements}
|
data={displayMeasurements}
|
||||||
onClick={jumpToImage}
|
onClick={jumpToImage}
|
||||||
@ -221,9 +227,17 @@ export default function PanelMeasurementTable({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center p-4">
|
<div className="flex justify-center p-4">
|
||||||
<ActionButtons
|
<ActionButtons
|
||||||
onExportClick={exportReport}
|
t={t('MeasurementTable')}
|
||||||
onClearMeasurementsClick={clearMeasurements}
|
actions={[
|
||||||
onCreateReportClick={createReport}
|
{
|
||||||
|
label: 'Export',
|
||||||
|
onClick: exportReport,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Create Report',
|
||||||
|
onClick: createReport,
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
import { Types, DisplaySetService, utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import { id } from '../id';
|
||||||
|
|
||||||
|
type InstanceMetadata = Types.InstanceMetadata;
|
||||||
|
|
||||||
|
const SOPClassHandlerName = 'chart';
|
||||||
|
|
||||||
|
const CHART_MODALITY = 'CHT';
|
||||||
|
|
||||||
|
// Private SOPClassUid for chart data
|
||||||
|
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
|
||||||
|
|
||||||
|
const sopClassUids = [ChartDataSOPClassUid];
|
||||||
|
|
||||||
|
const makeChartDataDisplaySet = (instance, sopClassUids) => {
|
||||||
|
const {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPClassUID,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
return {
|
||||||
|
Modality: CHART_MODALITY,
|
||||||
|
loading: false,
|
||||||
|
isReconstructable: false,
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
SeriesDescription,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDate,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPClassHandlerId: `${id}.sopClassHandlerModule.${SOPClassHandlerName}`,
|
||||||
|
SOPClassUID,
|
||||||
|
isDerivedDisplaySet: true,
|
||||||
|
isLoaded: true,
|
||||||
|
sopClassUids,
|
||||||
|
instance,
|
||||||
|
instances: [instance],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds instances to the chart displaySet, rather than creating a new one
|
||||||
|
* when user moves to a different workflow step and gets back to a step that
|
||||||
|
* recreates the chart
|
||||||
|
*/
|
||||||
|
addInstances: function (instances: InstanceMetadata[], _displaySetService: DisplaySetService) {
|
||||||
|
this.instances.push(...instances);
|
||||||
|
this.instance = this.instances[this.instances.length - 1];
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSopClassUids(instances) {
|
||||||
|
const uniqueSopClassUidsInSeries = new Set();
|
||||||
|
instances.forEach(instance => {
|
||||||
|
uniqueSopClassUidsInSeries.add(instance.SOPClassUID);
|
||||||
|
});
|
||||||
|
const sopClassUids = Array.from(uniqueSopClassUidsInSeries);
|
||||||
|
|
||||||
|
return sopClassUids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getDisplaySetsFromSeries(instances) {
|
||||||
|
debugger;
|
||||||
|
// If the series has no instances, stop here
|
||||||
|
if (!instances || !instances.length) {
|
||||||
|
throw new Error('No instances were provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopClassUids = getSopClassUids(instances);
|
||||||
|
const displaySets = instances.map(instance => {
|
||||||
|
if (instance.Modality === CHART_MODALITY) {
|
||||||
|
return makeChartDataDisplaySet(instance, sopClassUids);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unsupported modality');
|
||||||
|
});
|
||||||
|
|
||||||
|
return displaySets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartHandler = {
|
||||||
|
name: SOPClassHandlerName,
|
||||||
|
sopClassUids,
|
||||||
|
getDisplaySetsFromSeries: instances => {
|
||||||
|
return _getDisplaySetsFromSeries(instances);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export { chartHandler };
|
||||||
@ -3,10 +3,10 @@ import { Tooltip } from '@ohif/ui';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useToolbar } from '@ohif/core';
|
import { useToolbar } from '@ohif/core';
|
||||||
|
|
||||||
export function Toolbar({ servicesManager }) {
|
export function Toolbar({ servicesManager, buttonSection = 'primary' }) {
|
||||||
const { toolbarButtons, onInteraction } = useToolbar({
|
const { toolbarButtons, onInteraction } = useToolbar({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
buttonSection: 'primary',
|
buttonSection,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!toolbarButtons.length) {
|
if (!toolbarButtons.length) {
|
||||||
|
|||||||
@ -6,13 +6,11 @@ import { useLocation } from 'react-router';
|
|||||||
import { ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@ohif/ui';
|
import { ErrorBoundary, UserPreferences, AboutModal, Header, useModal } from '@ohif/ui';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import { hotkeys } from '@ohif/core';
|
import { hotkeys } from '@ohif/core';
|
||||||
import { useAppConfig } from '@state';
|
|
||||||
import { Toolbar } from '../Toolbar/Toolbar';
|
import { Toolbar } from '../Toolbar/Toolbar';
|
||||||
|
|
||||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||||
|
|
||||||
function ViewerHeader({ hotkeysManager, extensionManager, servicesManager }) {
|
function ViewerHeader({ hotkeysManager, extensionManager, servicesManager, appConfig }) {
|
||||||
const [appConfig] = useAppConfig();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -107,6 +105,13 @@ function ViewerHeader({ hotkeysManager, extensionManager, servicesManager }) {
|
|||||||
WhiteLabeling={appConfig.whiteLabeling}
|
WhiteLabeling={appConfig.whiteLabeling}
|
||||||
showPatientInfo={appConfig.showPatientInfo}
|
showPatientInfo={appConfig.showPatientInfo}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
|
Secondary={
|
||||||
|
<Toolbar
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
buttonSection="secondary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
appConfig={appConfig}
|
||||||
>
|
>
|
||||||
<ErrorBoundary context="Primary Toolbar">
|
<ErrorBoundary context="Primary Toolbar">
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import { ErrorBoundary, LoadingIndicatorProgress, InvestigationalUseDialog } from '@ohif/ui';
|
||||||
SidePanel,
|
|
||||||
ErrorBoundary,
|
|
||||||
LoadingIndicatorProgress,
|
|
||||||
InvestigationalUseDialog,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
import { ServicesManager, HangingProtocolService, CommandsManager } from '@ohif/core';
|
import { ServicesManager, HangingProtocolService, CommandsManager } from '@ohif/core';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import ViewerHeader from './ViewerHeader';
|
import ViewerHeader from './ViewerHeader';
|
||||||
@ -21,16 +16,24 @@ function ViewerLayout({
|
|||||||
// From Modes
|
// From Modes
|
||||||
viewports,
|
viewports,
|
||||||
ViewportGridComp,
|
ViewportGridComp,
|
||||||
leftPanels = [],
|
leftPanelClosed = false,
|
||||||
rightPanels = [],
|
rightPanelClosed = false,
|
||||||
leftPanelDefaultClosed = false,
|
|
||||||
rightPanelDefaultClosed = false,
|
|
||||||
}): React.FunctionComponent {
|
}): React.FunctionComponent {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const { hangingProtocolService } = servicesManager.services;
|
const { panelService, hangingProtocolService } = servicesManager.services;
|
||||||
const [showLoadingIndicator, setShowLoadingIndicator] = useState(appConfig.showLoadingIndicator);
|
const [showLoadingIndicator, setShowLoadingIndicator] = useState(appConfig.showLoadingIndicator);
|
||||||
|
|
||||||
|
const hasPanels = useCallback(
|
||||||
|
(side): boolean => !!panelService.getPanels(side).length,
|
||||||
|
[panelService]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [hasRightPanels, setHasRightPanels] = useState(hasPanels('right'));
|
||||||
|
const [hasLeftPanels, setHasLeftPanels] = useState(hasPanels('left'));
|
||||||
|
const [leftPanelClosedState, setLeftPanelClosed] = useState(leftPanelClosed);
|
||||||
|
const [rightPanelClosedState, setRightPanelClosed] = useState(rightPanelClosed);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set body classes (tailwindcss) that don't allow vertical
|
* Set body classes (tailwindcss) that don't allow vertical
|
||||||
* or horizontal overflow (no scrolling). Also guarantee window
|
* or horizontal overflow (no scrolling). Also guarantee window
|
||||||
@ -57,20 +60,6 @@ function ViewerLayout({
|
|||||||
return { entry, content: entry.component };
|
return { entry, content: entry.component };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPanelData = id => {
|
|
||||||
const { content, entry } = getComponent(id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: entry.id,
|
|
||||||
iconName: entry.iconName,
|
|
||||||
iconLabel: entry.iconLabel,
|
|
||||||
label: entry.label,
|
|
||||||
name: entry.name,
|
|
||||||
content,
|
|
||||||
contexts: entry.contexts,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||||
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||||
@ -97,8 +86,26 @@ function ViewerLayout({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const leftPanelComponents = leftPanels.map(getPanelData);
|
useEffect(() => {
|
||||||
const rightPanelComponents = rightPanels.map(getPanelData);
|
const { unsubscribe } = panelService.subscribe(
|
||||||
|
panelService.EVENTS.PANELS_CHANGED,
|
||||||
|
({ options }) => {
|
||||||
|
setHasLeftPanels(hasPanels('left'));
|
||||||
|
setHasRightPanels(hasPanels('right'));
|
||||||
|
if (options?.leftPanelClosed !== undefined) {
|
||||||
|
setLeftPanelClosed(options.leftPanelClosed);
|
||||||
|
}
|
||||||
|
if (options?.rightPanelClosed !== undefined) {
|
||||||
|
setRightPanelClosed(options.rightPanelClosed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [panelService, hasPanels]);
|
||||||
|
|
||||||
const viewportComponents = viewports.map(getViewportComponentData);
|
const viewportComponents = viewports.map(getViewportComponentData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -107,6 +114,7 @@ function ViewerLayout({
|
|||||||
hotkeysManager={hotkeysManager}
|
hotkeysManager={hotkeysManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
|
appConfig={appConfig}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="relative flex w-full flex-row flex-nowrap items-stretch overflow-hidden bg-black"
|
className="relative flex w-full flex-row flex-nowrap items-stretch overflow-hidden bg-black"
|
||||||
@ -115,12 +123,11 @@ function ViewerLayout({
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-black" />}
|
{showLoadingIndicator && <LoadingIndicatorProgress className="h-full w-full bg-black" />}
|
||||||
{/* LEFT SIDEPANELS */}
|
{/* LEFT SIDEPANELS */}
|
||||||
{leftPanelComponents.length ? (
|
{hasLeftPanels ? (
|
||||||
<ErrorBoundary context="Left Panel">
|
<ErrorBoundary context="Left Panel">
|
||||||
<SidePanelWithServices
|
<SidePanelWithServices
|
||||||
side="left"
|
side="left"
|
||||||
activeTabIndex={leftPanelDefaultClosed ? null : 0}
|
activeTabIndex={leftPanelClosedState ? null : 0}
|
||||||
tabs={leftPanelComponents}
|
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@ -137,12 +144,11 @@ function ViewerLayout({
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{rightPanelComponents.length ? (
|
{hasRightPanels ? (
|
||||||
<ErrorBoundary context="Right Panel">
|
<ErrorBoundary context="Right Panel">
|
||||||
<SidePanelWithServices
|
<SidePanelWithServices
|
||||||
side="right"
|
side="right"
|
||||||
activeTabIndex={rightPanelDefaultClosed ? null : 0}
|
activeTabIndex={rightPanelClosedState ? null : 0}
|
||||||
tabs={rightPanelComponents}
|
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@ -165,8 +171,8 @@ ViewerLayout.propTypes = {
|
|||||||
// From modes
|
// From modes
|
||||||
leftPanels: PropTypes.array,
|
leftPanels: PropTypes.array,
|
||||||
rightPanels: PropTypes.array,
|
rightPanels: PropTypes.array,
|
||||||
leftPanelDefaultClosed: PropTypes.bool.isRequired,
|
leftPanelClosed: PropTypes.bool.isRequired,
|
||||||
rightPanelDefaultClosed: PropTypes.bool.isRequired,
|
rightPanelClosed: PropTypes.bool.isRequired,
|
||||||
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
||||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||||
viewports: PropTypes.array,
|
viewports: PropTypes.array,
|
||||||
|
|||||||
@ -26,15 +26,6 @@ export type UpdateViewportDisplaySetParams = {
|
|||||||
excludeNonImageModalities?: boolean;
|
excludeNonImageModalities?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a command is a hanging protocol one.
|
|
||||||
* For now, just use the two hanging protocol commands that are in this
|
|
||||||
* commands module, but if others get added elsewhere this may need enhancing.
|
|
||||||
*/
|
|
||||||
const isHangingProtocolCommand = command =>
|
|
||||||
command &&
|
|
||||||
(command.commandName === 'setHangingProtocol' || command.commandName === 'toggleHangingProtocol');
|
|
||||||
|
|
||||||
const commandsModule = ({
|
const commandsModule = ({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -47,7 +38,6 @@ const commandsModule = ({
|
|||||||
viewportGridService,
|
viewportGridService,
|
||||||
displaySetService,
|
displaySetService,
|
||||||
stateSyncService,
|
stateSyncService,
|
||||||
toolbarService,
|
|
||||||
} = (servicesManager as ServicesManager).services;
|
} = (servicesManager as ServicesManager).services;
|
||||||
|
|
||||||
// Define a context menu controller for use with any context menus
|
// Define a context menu controller for use with any context menus
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
|
||||||
|
import { ServicesManager } from '@ohif/core';
|
||||||
|
import { ProgressDropdown } from '@ohif/ui';
|
||||||
|
|
||||||
|
const workflowStepsToDropdownOptions = (steps = []) =>
|
||||||
|
steps.map(step => ({
|
||||||
|
label: step.name,
|
||||||
|
value: step.id,
|
||||||
|
info: step.info,
|
||||||
|
activated: false,
|
||||||
|
completed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function ProgressDropdownWithService({
|
||||||
|
servicesManager,
|
||||||
|
}: {
|
||||||
|
servicesManager: ServicesManager;
|
||||||
|
}): ReactElement {
|
||||||
|
const { workflowStepsService } = servicesManager.services;
|
||||||
|
const [activeStepId, setActiveStepId] = useState(workflowStepsService.activeWorkflowStep?.id);
|
||||||
|
|
||||||
|
const [dropdownOptions, setDropdownOptions] = useState(
|
||||||
|
workflowStepsToDropdownOptions(workflowStepsService.workflowSteps)
|
||||||
|
);
|
||||||
|
|
||||||
|
const setCurrentAndPreviousOptionsAsCompleted = useCallback(currentOption => {
|
||||||
|
if (currentOption.completed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDropdownOptions(prevOptions => {
|
||||||
|
const newOptionsState = [...prevOptions];
|
||||||
|
const startIndex = newOptionsState.findIndex(option => option.value === currentOption.value);
|
||||||
|
|
||||||
|
for (let i = startIndex; i >= 0; i--) {
|
||||||
|
const option = newOptionsState[i];
|
||||||
|
|
||||||
|
if (option.completed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
newOptionsState[i] = {
|
||||||
|
...option,
|
||||||
|
completed: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOptionsState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDropdownChange = useCallback(
|
||||||
|
({ selectedOption }) => {
|
||||||
|
if (!selectedOption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Steps should be marked as completed after user has
|
||||||
|
// completed some action when required (not implemented)
|
||||||
|
setCurrentAndPreviousOptionsAsCompleted(selectedOption);
|
||||||
|
setActiveStepId(selectedOption.value);
|
||||||
|
},
|
||||||
|
[setCurrentAndPreviousOptionsAsCompleted]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
if (activeStepId) {
|
||||||
|
// We've used setTimeout to give it more time to update the UI since
|
||||||
|
// create3DFilterableFromDataArray from Texture.js may take 600+ ms to run
|
||||||
|
// when there is a new series to load in the next step but that resulted
|
||||||
|
// in the followed React error when updating the content from left/right panels
|
||||||
|
// and all component states were being lost:
|
||||||
|
// Error: Can't perform a React state update on an unmounted component
|
||||||
|
workflowStepsService.setActiveWorkflowStep(activeStepId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [activeStepId, workflowStepsService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { unsubscribe: unsubStepsChanged } = workflowStepsService.subscribe(
|
||||||
|
workflowStepsService.EVENTS.STEPS_CHANGED,
|
||||||
|
() => setDropdownOptions(workflowStepsToDropdownOptions(workflowStepsService.workflowSteps))
|
||||||
|
);
|
||||||
|
|
||||||
|
const { unsubscribe: unsubActiveStepChanged } = workflowStepsService.subscribe(
|
||||||
|
workflowStepsService.EVENTS.ACTIVE_STEP_CHANGED,
|
||||||
|
|
||||||
|
() => setActiveStepId(workflowStepsService.activeWorkflowStep.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubStepsChanged();
|
||||||
|
unsubActiveStepChanged();
|
||||||
|
};
|
||||||
|
}, [servicesManager, workflowStepsService]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProgressDropdown
|
||||||
|
options={dropdownOptions}
|
||||||
|
value={activeStepId}
|
||||||
|
onChange={handleDropdownChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProgressDropdownWithService;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from './ProgressDropdownWithService';
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { CustomizationService } from '@ohif/core';
|
import { CustomizationService } from '@ohif/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DataSourceSelector from './Panels/DataSourceSelector';
|
import DataSourceSelector from './Panels/DataSourceSelector';
|
||||||
|
import ProgressDropdownWithService from './components/ProgressDropdownWithService';
|
||||||
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
|
import DataSourceConfigurationComponent from './Components/DataSourceConfigurationComponent';
|
||||||
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
import { GoogleCloudDataSourceConfigurationAPI } from './DataSourceConfigurationAPI/GoogleCloudDataSourceConfigurationAPI';
|
||||||
|
|
||||||
@ -154,6 +155,11 @@ export default function getCustomizationModule({ servicesManager, extensionManag
|
|||||||
extensionManager
|
extensionManager
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'progressDropdownWithServiceComponent',
|
||||||
|
component: ProgressDropdownWithService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -10,9 +10,15 @@ import checkSingleFrames from './utils/validations/checkSingleFrames';
|
|||||||
*/
|
*/
|
||||||
export default function getDisplaySetMessages(
|
export default function getDisplaySetMessages(
|
||||||
instances: Array<any>,
|
instances: Array<any>,
|
||||||
isReconstructable: boolean
|
isReconstructable: boolean,
|
||||||
|
isDynamicVolume: boolean
|
||||||
): DisplaySetMessageList {
|
): DisplaySetMessageList {
|
||||||
const messages = new DisplaySetMessageList();
|
const messages = new DisplaySetMessageList();
|
||||||
|
|
||||||
|
if (isDynamicVolume) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
if (!instances.length) {
|
if (!instances.length) {
|
||||||
messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES);
|
messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -5,23 +5,79 @@ import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetRecons
|
|||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
import getDisplaySetMessages from './getDisplaySetMessages';
|
import getDisplaySetMessages from './getDisplaySetMessages';
|
||||||
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
|
import getDisplaySetsFromUnsupportedSeries from './getDisplaySetsFromUnsupportedSeries';
|
||||||
|
import { chartHandler } from './SOPClassHandlers/chartSOPClassHandler';
|
||||||
|
|
||||||
|
const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||||
|
const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
|
||||||
const sopClassHandlerName = 'stack';
|
const sopClassHandlerName = 'stack';
|
||||||
|
let appContext = {};
|
||||||
|
|
||||||
|
const getDynamicVolumeInfo = instances => {
|
||||||
|
const { extensionManager } = appContext;
|
||||||
|
|
||||||
|
if (!extensionManager) {
|
||||||
|
throw new Error('extensionManager is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = instances.map(({ imageId }) => imageId);
|
||||||
|
const volumeLoaderUtility = extensionManager.getModuleEntry(
|
||||||
|
'@ohif/extension-cornerstone.utilityModule.volumeLoader'
|
||||||
|
);
|
||||||
|
const { getDynamicVolumeInfo: csGetDynamicVolumeInfo } = volumeLoaderUtility.exports;
|
||||||
|
|
||||||
|
return csGetDynamicVolumeInfo(imageIds);
|
||||||
|
};
|
||||||
|
|
||||||
const isMultiFrame = instance => {
|
const isMultiFrame = instance => {
|
||||||
return instance.NumberOfFrames > 1;
|
return instance.NumberOfFrames > 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getDisplaySetInfo(instances) {
|
||||||
|
const dynamicVolumeInfo = getDynamicVolumeInfo(instances);
|
||||||
|
const { isDynamicVolume, timePoints } = dynamicVolumeInfo;
|
||||||
|
let displaySetInfo;
|
||||||
|
|
||||||
|
if (isDynamicVolume) {
|
||||||
|
const timePoint = timePoints[0];
|
||||||
|
const instancesMap = new Map();
|
||||||
|
|
||||||
|
// O(n) to convert it into a map and O(1) to find each instance
|
||||||
|
instances.forEach(instance => instancesMap.set(instance.imageId, instance));
|
||||||
|
|
||||||
|
const firstTimePointInstances = timePoint.map(imageId => instancesMap.get(imageId));
|
||||||
|
|
||||||
|
displaySetInfo = isDisplaySetReconstructable(firstTimePointInstances);
|
||||||
|
} else {
|
||||||
|
displaySetInfo = isDisplaySetReconstructable(instances);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDynamicVolume,
|
||||||
|
...displaySetInfo,
|
||||||
|
dynamicVolumeInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const makeDisplaySet = instances => {
|
const makeDisplaySet = instances => {
|
||||||
const instance = instances[0];
|
const instance = instances[0];
|
||||||
const imageSet = new ImageSet(instances);
|
const imageSet = new ImageSet(instances);
|
||||||
|
|
||||||
const { value: isReconstructable, averageSpacingBetweenFrames } =
|
const {
|
||||||
isDisplaySetReconstructable(instances);
|
isDynamicVolume,
|
||||||
|
value: isReconstructable,
|
||||||
|
averageSpacingBetweenFrames,
|
||||||
|
dynamicVolumeInfo,
|
||||||
|
} = getDisplaySetInfo(instances);
|
||||||
|
|
||||||
|
const volumeLoaderSchema = isDynamicVolume
|
||||||
|
? DYNAMIC_VOLUME_LOADER_SCHEME
|
||||||
|
: DEFAULT_VOLUME_LOADER_SCHEME;
|
||||||
|
|
||||||
// set appropriate attributes to image set...
|
// set appropriate attributes to image set...
|
||||||
const messages = getDisplaySetMessages(instances, isReconstructable);
|
const messages = getDisplaySetMessages(instances, isReconstructable, isDynamicVolume);
|
||||||
|
|
||||||
imageSet.setAttributes({
|
imageSet.setAttributes({
|
||||||
|
volumeLoaderSchema,
|
||||||
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||||
SeriesDate: instance.SeriesDate,
|
SeriesDate: instance.SeriesDate,
|
||||||
SeriesTime: instance.SeriesTime,
|
SeriesTime: instance.SeriesTime,
|
||||||
@ -39,6 +95,8 @@ const makeDisplaySet = instances => {
|
|||||||
isReconstructable,
|
isReconstructable,
|
||||||
messages,
|
messages,
|
||||||
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
||||||
|
isDynamicVolume,
|
||||||
|
dynamicVolumeInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort the images in this series if needed
|
// Sort the images in this series if needed
|
||||||
@ -88,7 +146,6 @@ function getSopClassUids(instances) {
|
|||||||
* - For all Image types that are stackable, create
|
* - For all Image types that are stackable, create
|
||||||
* a displaySet with a stack of images
|
* a displaySet with a stack of images
|
||||||
*
|
*
|
||||||
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
|
||||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
* @returns {Array} The list of display sets created for the given series object
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
*/
|
*/
|
||||||
@ -203,7 +260,9 @@ const sopClassUids = [
|
|||||||
sopClassDictionary.EnhancedUSVolumeStorage,
|
sopClassDictionary.EnhancedUSVolumeStorage,
|
||||||
];
|
];
|
||||||
|
|
||||||
function getSopClassHandlerModule() {
|
function getSopClassHandlerModule(appContextParam) {
|
||||||
|
appContext = appContextParam;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: sopClassHandlerName,
|
name: sopClassHandlerName,
|
||||||
@ -215,6 +274,11 @@ function getSopClassHandlerModule() {
|
|||||||
sopClassUids: [],
|
sopClassUids: [],
|
||||||
getDisplaySetsFromSeries: getDisplaySetsFromUnsupportedSeries,
|
getDisplaySetsFromSeries: getDisplaySetsFromUnsupportedSeries,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: chartHandler.name,
|
||||||
|
sopClassUids: chartHandler.sopClassUids,
|
||||||
|
getDisplaySetsFromSeries: chartHandler.getDisplaySetsFromSeries,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import ToolbarLayoutSelectorWithServices from './Toolbar/ToolbarLayoutSelector';
|
|||||||
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
|
import ToolbarSplitButtonWithServices from './Toolbar/ToolbarSplitButtonWithServices';
|
||||||
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
|
import ToolbarButtonGroupWithServices from './Toolbar/ToolbarButtonGroupWithServices';
|
||||||
import { ToolbarButton } from '@ohif/ui';
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
|
import ProgressDropdownWithService from './components/ProgressDropdownWithService';
|
||||||
|
|
||||||
const getClassName = isToggled => {
|
const getClassName = isToggled => {
|
||||||
return {
|
return {
|
||||||
@ -36,6 +37,10 @@ export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|||||||
name: 'ohif.buttonGroup',
|
name: 'ohif.buttonGroup',
|
||||||
defaultComponent: ToolbarButtonGroupWithServices,
|
defaultComponent: ToolbarButtonGroupWithServices,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'ohif.progressDropdown',
|
||||||
|
defaultComponent: ProgressDropdownWithService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'evaluate.group.promoteToPrimary',
|
name: 'evaluate.group.promoteToPrimary',
|
||||||
evaluate: ({ viewportId, button, itemId }) => {
|
evaluate: ({ viewportId, button, itemId }) => {
|
||||||
|
|||||||
21
extensions/default/src/getViewportModule.tsx
Normal file
21
extensions/default/src/getViewportModule.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ServicesManager, CommandsManager, ExtensionManager } from '@ohif/core';
|
||||||
|
import LineChartViewport from './Components/LineChartViewport/index';
|
||||||
|
|
||||||
|
const getViewportModule = ({
|
||||||
|
servicesManager,
|
||||||
|
commandsManager,
|
||||||
|
extensionManager,
|
||||||
|
}: {
|
||||||
|
servicesManager: ServicesManager;
|
||||||
|
commandsManager: CommandsManager;
|
||||||
|
extensionManager: ExtensionManager;
|
||||||
|
}) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'chartViewport',
|
||||||
|
component: LineChartViewport,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getViewportModule as default };
|
||||||
@ -9,6 +9,7 @@ import getCommandsModule from './commandsModule';
|
|||||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getStudiesForPatientByMRN from './Panels/getStudiesForPatientByMRN';
|
import getStudiesForPatientByMRN from './Panels/getStudiesForPatientByMRN';
|
||||||
import getCustomizationModule from './getCustomizationModule';
|
import getCustomizationModule from './getCustomizationModule';
|
||||||
|
import getViewportModule from './getViewportModule';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import preRegistration from './init';
|
import preRegistration from './init';
|
||||||
import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu';
|
import { ContextMenuController, CustomizableContextMenuTypes } from './CustomizableContextMenu';
|
||||||
@ -23,6 +24,7 @@ const defaultExtension: Types.Extensions.Extension = {
|
|||||||
id,
|
id,
|
||||||
preRegistration,
|
preRegistration,
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
|
getViewportModule,
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getHangingProtocolModule,
|
getHangingProtocolModule,
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export default function init({ servicesManager, configuration = {}, commandsMana
|
|||||||
toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => {
|
toolbarService.subscribe(toolbarService.EVENTS.TOOL_BAR_MODIFIED, state => {
|
||||||
const { buttons } = state;
|
const { buttons } = state;
|
||||||
for (const [id, button] of Object.entries(buttons)) {
|
for (const [id, button] of Object.entries(buttons)) {
|
||||||
const { groupId, items, listeners } = button.props;
|
const { groupId, items, listeners } = button.props || {};
|
||||||
|
|
||||||
// Handle group items' listeners
|
// Handle group items' listeners
|
||||||
if (groupId && items) {
|
if (groupId && items) {
|
||||||
|
|||||||
@ -21,6 +21,11 @@ const reuseCachedLayout = (
|
|||||||
): ReturnType => {
|
): ReturnType => {
|
||||||
const { activeViewportId } = state;
|
const { activeViewportId } = state;
|
||||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||||
|
|
||||||
|
if (!protocol) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hpInfo = hangingProtocolService.getState();
|
const hpInfo = hangingProtocolService.getState();
|
||||||
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
|
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
|
||||||
|
|
||||||
|
|||||||
@ -32,8 +32,8 @@
|
|||||||
"start": "yarn run dev"
|
"start": "yarn run dev"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@cornerstonejs/core": "^1.68.1",
|
"@cornerstonejs/core": "^1.70.0",
|
||||||
"@cornerstonejs/tools": "^1.68.1",
|
"@cornerstonejs/tools": "^1.70.0",
|
||||||
"@ohif/core": "3.8.0-beta.73",
|
"@ohif/core": "3.8.0-beta.73",
|
||||||
"@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.73",
|
"@ohif/extension-cornerstone-dicom-sr": "3.8.0-beta.73",
|
||||||
"@ohif/ui": "3.8.0-beta.73",
|
"@ohif/ui": "3.8.0-beta.73",
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Button, ButtonEnums } from '@ohif/ui';
|
|
||||||
|
|
||||||
function ActionButtons({ onExportClick, onCreateReportClick, disabled }) {
|
|
||||||
const { t } = useTranslation('MeasurementTable');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<Button
|
|
||||||
onClick={onExportClick}
|
|
||||||
disabled={disabled}
|
|
||||||
type={ButtonEnums.type.secondary}
|
|
||||||
size={ButtonEnums.size.small}
|
|
||||||
>
|
|
||||||
{t('Export')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="ml-2"
|
|
||||||
onClick={onCreateReportClick}
|
|
||||||
type={ButtonEnums.type.secondary}
|
|
||||||
size={ButtonEnums.size.small}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{t('Create Report')}
|
|
||||||
</Button>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ActionButtons.propTypes = {
|
|
||||||
onExportClick: PropTypes.func,
|
|
||||||
onCreateReportClick: PropTypes.func,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
ActionButtons.defaultProps = {
|
|
||||||
onExportClick: () => alert('Export'),
|
|
||||||
onCreateReportClick: () => alert('Create Report'),
|
|
||||||
disabled: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionButtons;
|
|
||||||
@ -1,19 +1,12 @@
|
|||||||
import React, { useEffect, useState, useRef } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { StudySummary, MeasurementTable, useViewportGrid, ActionButtons } from '@ohif/ui';
|
||||||
StudySummary,
|
|
||||||
MeasurementTable,
|
|
||||||
Dialog,
|
|
||||||
Input,
|
|
||||||
useViewportGrid,
|
|
||||||
ButtonEnums,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||||
import { useDebounce } from '@hooks';
|
import { useDebounce } from '@hooks';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
import ActionButtons from './ActionButtons';
|
|
||||||
import { useTrackedMeasurements } from '../../getContextModule';
|
import { useTrackedMeasurements } from '../../getContextModule';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const { downloadCSVReport } = utils;
|
const { downloadCSVReport } = utils;
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
@ -27,9 +20,11 @@ const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
|
|||||||
|
|
||||||
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||||
const [viewportGrid] = useViewportGrid();
|
const [viewportGrid] = useViewportGrid();
|
||||||
|
const { t } = useTranslation('MeasurementTable');
|
||||||
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString());
|
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(Date.now().toString());
|
||||||
const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200);
|
const debouncedMeasurementChangeTimestamp = useDebounce(measurementChangeTimestamp, 200);
|
||||||
const { measurementService, uiDialogService, displaySetService, customizationService } = servicesManager.services;
|
const { measurementService, uiDialogService, displaySetService, customizationService } =
|
||||||
|
servicesManager.services;
|
||||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||||
const [displayStudySummary, setDisplayStudySummary] = useState(
|
const [displayStudySummary, setDisplayStudySummary] = useState(
|
||||||
@ -172,6 +167,9 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
dm => dm.measurementType === measurementService.VALUE_TYPES.POINT
|
dm => dm.measurementType === measurementService.VALUE_TYPES.POINT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const disabled =
|
||||||
|
additionalFindings.length === 0 && displayMeasurementsWithoutFindings.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@ -206,16 +204,23 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
{!appConfig?.disableEditing && (
|
{!appConfig?.disableEditing && (
|
||||||
<div className="flex justify-center p-4">
|
<div className="flex justify-center p-4">
|
||||||
<ActionButtons
|
<ActionButtons
|
||||||
onExportClick={exportReport}
|
t={t}
|
||||||
onCreateReportClick={() => {
|
actions={[
|
||||||
sendTrackedMeasurementsEvent('SAVE_REPORT', {
|
{
|
||||||
viewportId: viewportGrid.activeViewportId,
|
label: 'Export',
|
||||||
isBackupSave: true,
|
onClick: exportReport,
|
||||||
});
|
},
|
||||||
}}
|
{
|
||||||
disabled={
|
label: 'Create Report',
|
||||||
additionalFindings.length === 0 && displayMeasurementsWithoutFindings.length === 0
|
onClick: () => {
|
||||||
}
|
sendTrackedMeasurementsEvent('SAVE_REPORT', {
|
||||||
|
viewportId: viewportGrid.activeViewportId,
|
||||||
|
isBackupSave: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,8 +19,13 @@ function PanelStudyBrowserTracking({
|
|||||||
requestDisplaySetCreationForStudy,
|
requestDisplaySetCreationForStudy,
|
||||||
dataSource,
|
dataSource,
|
||||||
}) {
|
}) {
|
||||||
const { displaySetService, uiDialogService, hangingProtocolService, uiNotificationService, measurementService } =
|
const {
|
||||||
servicesManager.services;
|
displaySetService,
|
||||||
|
uiDialogService,
|
||||||
|
hangingProtocolService,
|
||||||
|
uiNotificationService,
|
||||||
|
measurementService,
|
||||||
|
} = servicesManager.services;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { t } = useTranslation('Common');
|
const { t } = useTranslation('Common');
|
||||||
@ -129,7 +134,8 @@ function PanelStudyBrowserTracking({
|
|||||||
const newImageSrcEntry = {};
|
const newImageSrcEntry = {};
|
||||||
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
const displaySet = displaySetService.getDisplaySetByUID(dSet.displaySetInstanceUID);
|
||||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
|
||||||
|
const imageId = getImageIdForThumbnail(displaySet, imageIds);
|
||||||
|
|
||||||
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
|
// TODO: Is it okay that imageIds are not returned here for SR displaySets?
|
||||||
if (!imageId || displaySet?.unsupported) {
|
if (!imageId || displaySet?.unsupported) {
|
||||||
@ -195,7 +201,7 @@ function PanelStudyBrowserTracking({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
const imageId = getImageIdForThumbnail(displaySet, imageIds);
|
||||||
|
|
||||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||||
if (!imageId) {
|
if (!imageId) {
|
||||||
@ -366,6 +372,19 @@ PanelStudyBrowserTracking.propTypes = {
|
|||||||
|
|
||||||
export default PanelStudyBrowserTracking;
|
export default PanelStudyBrowserTracking;
|
||||||
|
|
||||||
|
function getImageIdForThumbnail(displaySet: any, imageIds: any) {
|
||||||
|
let imageId;
|
||||||
|
if (displaySet.isDynamicVolume) {
|
||||||
|
const timePoints = displaySet.dynamicVolumeInfo.timePoints;
|
||||||
|
const middleIndex = Math.floor(timePoints.length / 2);
|
||||||
|
const middleTimePointImageIds = timePoints[middleIndex];
|
||||||
|
imageId = middleTimePointImageIds[Math.floor(middleTimePointImageIds.length / 2)];
|
||||||
|
} else {
|
||||||
|
imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||||
|
}
|
||||||
|
return imageId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps from the DataSource's format to a naturalized object
|
* Maps from the DataSource's format to a naturalized object
|
||||||
*
|
*
|
||||||
@ -631,4 +650,4 @@ function _findTabAndStudyOfDisplaySet(displaySetInstanceUID, tabs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,17 +140,18 @@ function TrackedCornerstoneViewport(props) {
|
|||||||
// Only send the tracked measurements event for the active viewport to avoid
|
// Only send the tracked measurements event for the active viewport to avoid
|
||||||
// sending it more than once.
|
// sending it more than once.
|
||||||
if (viewportId === activeViewportId) {
|
if (viewportId === activeViewportId) {
|
||||||
const { referenceStudyUID: StudyInstanceUID,
|
const {
|
||||||
|
referenceStudyUID: StudyInstanceUID,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
uid: measurementId } =
|
uid: measurementId,
|
||||||
measurement;
|
} = measurement;
|
||||||
|
|
||||||
sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
|
sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
|
||||||
sendTrackedMeasurementsEvent('TRACK_SERIES', {
|
sendTrackedMeasurementsEvent('TRACK_SERIES', {
|
||||||
viewportId,
|
viewportId,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
measurementId
|
measurementId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).unsubscribe
|
}).unsubscribe
|
||||||
|
|||||||
17
extensions/test-extension/src/hp/index.ts
Normal file
17
extensions/test-extension/src/hp/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import hpMN from './hpMN';
|
||||||
|
|
||||||
|
const hangingProtocols = [
|
||||||
|
{
|
||||||
|
name: '@ohif/hp-extension.mn',
|
||||||
|
protocol: hpMN,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a single study hanging protocol which can be referenced as
|
||||||
|
* `@ohif/hp-exgtension.mn`, that has initial layouts which show images
|
||||||
|
* only display sets, up to a 2x2 view.
|
||||||
|
*/
|
||||||
|
export default function getHangingProtocolModule() {
|
||||||
|
return hangingProtocols;
|
||||||
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { LegacyButton, LegacyButtonGroup } from '@ohif/ui';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
function ExportReports({ segmentations, tmtvValue, config, commandsManager }) {
|
|
||||||
const { t } = useTranslation('PanelSUVExport');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{segmentations?.length ? (
|
|
||||||
<div className="mt-4 flex justify-center space-x-2">
|
|
||||||
{/* TODO Revisit design of LegacyButtonGroup later - for now use LegacyButton for its children.*/}
|
|
||||||
<LegacyButtonGroup
|
|
||||||
color="black"
|
|
||||||
size="inherit"
|
|
||||||
>
|
|
||||||
<LegacyButton
|
|
||||||
className="px-2 py-2 text-base"
|
|
||||||
disabled={tmtvValue === null}
|
|
||||||
onClick={() => {
|
|
||||||
commandsManager.runCommand('exportTMTVReportCSV', {
|
|
||||||
segmentations,
|
|
||||||
tmtv: tmtvValue,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Export CSV')}
|
|
||||||
</LegacyButton>
|
|
||||||
</LegacyButtonGroup>
|
|
||||||
<LegacyButtonGroup
|
|
||||||
color="black"
|
|
||||||
size="inherit"
|
|
||||||
>
|
|
||||||
<LegacyButton
|
|
||||||
className="px-2 py-2 text-base"
|
|
||||||
onClick={() => {
|
|
||||||
commandsManager.runCommand('createTMTVRTReport');
|
|
||||||
}}
|
|
||||||
disabled={tmtvValue === null}
|
|
||||||
>
|
|
||||||
{t('Create RT Report')}
|
|
||||||
</LegacyButton>
|
|
||||||
</LegacyButtonGroup>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExportReports;
|
|
||||||
@ -1,287 +0,0 @@
|
|||||||
import React, { useEffect, useState, useCallback, useReducer } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { SegmentationTable, Button, Icon } from '@ohif/ui';
|
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import segmentationEditHandler from './segmentationEditHandler';
|
|
||||||
import ExportReports from './ExportReports';
|
|
||||||
import ROIThresholdConfiguration, { ROI_STAT } from './ROIThresholdConfiguration';
|
|
||||||
|
|
||||||
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
|
||||||
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
|
||||||
const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
|
|
||||||
const UPPER_PT_THRESHOLD_DEFAULT = 100;
|
|
||||||
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
|
|
||||||
const DEFAULT_STRATEGY = ROI_STAT;
|
|
||||||
|
|
||||||
function reducer(state, action) {
|
|
||||||
const { payload } = action;
|
|
||||||
const { strategy, ctLower, ctUpper, ptLower, ptUpper, weight } = payload;
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case 'setStrategy':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
strategy,
|
|
||||||
};
|
|
||||||
case 'setThreshold':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
ctLower: ctLower ? ctLower : state.ctLower,
|
|
||||||
ctUpper: ctUpper ? ctUpper : state.ctUpper,
|
|
||||||
ptLower: ptLower ? ptLower : state.ptLower,
|
|
||||||
ptUpper: ptUpper ? ptUpper : state.ptUpper,
|
|
||||||
};
|
|
||||||
case 'setWeight':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
weight,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LegacyPanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
|
|
||||||
const { segmentationService } = servicesManager.services;
|
|
||||||
|
|
||||||
const { t } = useTranslation('PanelSUV');
|
|
||||||
const [showConfig, setShowConfig] = useState(false);
|
|
||||||
const [labelmapLoading, setLabelmapLoading] = useState(false);
|
|
||||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
|
||||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
|
||||||
|
|
||||||
const [config, dispatch] = useReducer(reducer, {
|
|
||||||
strategy: DEFAULT_STRATEGY,
|
|
||||||
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
|
|
||||||
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
|
|
||||||
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
|
|
||||||
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
|
|
||||||
weight: WEIGHT_DEFAULT,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [tmtvValue, setTmtvValue] = useState(null);
|
|
||||||
|
|
||||||
const runCommand = useCallback(
|
|
||||||
(commandName, commandOptions = {}) => {
|
|
||||||
return commandsManager.runCommand(commandName, commandOptions);
|
|
||||||
},
|
|
||||||
[commandsManager]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTMTVCalculation = useCallback(() => {
|
|
||||||
const tmtv = runCommand('calculateTMTV', { segmentations });
|
|
||||||
|
|
||||||
if (tmtv !== undefined) {
|
|
||||||
setTmtvValue(tmtv.toFixed(2));
|
|
||||||
}
|
|
||||||
}, [segmentations, runCommand]);
|
|
||||||
|
|
||||||
const handleROIThresholding = useCallback(() => {
|
|
||||||
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
|
||||||
segmentationId: selectedSegmentationId,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const lesionStats = runCommand('getLesionStats', { labelmap });
|
|
||||||
const suvPeak = runCommand('calculateSuvPeak', { labelmap });
|
|
||||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
|
||||||
|
|
||||||
// update segDetails with the suv peak for the active segmentation
|
|
||||||
const segmentation = segmentationService.getSegmentation(selectedSegmentationId);
|
|
||||||
|
|
||||||
const cachedStats = {
|
|
||||||
lesionStats,
|
|
||||||
suvPeak,
|
|
||||||
lesionGlyoclysisStats,
|
|
||||||
};
|
|
||||||
|
|
||||||
const notYetUpdatedAtSource = true;
|
|
||||||
segmentationService.addOrUpdateSegmentation(
|
|
||||||
{
|
|
||||||
...segmentation,
|
|
||||||
...Object.assign(segmentation.cachedStats, cachedStats),
|
|
||||||
displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
|
|
||||||
},
|
|
||||||
notYetUpdatedAtSource
|
|
||||||
);
|
|
||||||
|
|
||||||
handleTMTVCalculation();
|
|
||||||
}, [selectedSegmentationId, config]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update UI based on segmentation changes (added, removed, updated)
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
// ~~ Subscription
|
|
||||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
|
||||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
|
||||||
const subscriptions = [];
|
|
||||||
|
|
||||||
[added, updated].forEach(evt => {
|
|
||||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
setSegmentations(segmentations);
|
|
||||||
});
|
|
||||||
subscriptions.push(unsubscribe);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscriptions.forEach(unsub => {
|
|
||||||
unsub();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { unsubscribe } = segmentationService.subscribe(
|
|
||||||
segmentationService.EVENTS.SEGMENTATION_REMOVED,
|
|
||||||
() => {
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
setSegmentations(segmentations);
|
|
||||||
|
|
||||||
if (segmentations.length > 0) {
|
|
||||||
setSelectedSegmentationId(segmentations[0].id);
|
|
||||||
handleTMTVCalculation();
|
|
||||||
} else {
|
|
||||||
setSelectedSegmentationId(null);
|
|
||||||
setTmtvValue(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whenever the segmentations change, update the TMTV calculations
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedSegmentationId && segmentations.length > 0) {
|
|
||||||
setSelectedSegmentationId(segmentations[0].id);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTMTVCalculation();
|
|
||||||
}, [segmentations, selectedSegmentationId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
|
||||||
<div className="mx-4 my-4 mb-4 flex space-x-4">
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setLabelmapLoading(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
|
||||||
setLabelmapLoading(false);
|
|
||||||
setSelectedSegmentationId(segmentationId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{labelmapLoading ? 'loading ...' : 'New Label'}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleROIThresholding}>Run</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="bg-secondary-dark border-secondary-light mb-2 flex h-8 cursor-pointer select-none items-center justify-around border-t outline-none first:border-0"
|
|
||||||
onClick={() => {
|
|
||||||
setShowConfig(!showConfig);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="px-4 text-base text-white">{t('ROI Threshold Configuration')}</div>
|
|
||||||
</div>
|
|
||||||
{showConfig && (
|
|
||||||
<ROIThresholdConfiguration
|
|
||||||
config={config}
|
|
||||||
dispatch={dispatch}
|
|
||||||
runCommand={runCommand}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* show segmentation table */}
|
|
||||||
<div className="mt-4">
|
|
||||||
{segmentations?.length ? (
|
|
||||||
<SegmentationTable
|
|
||||||
title={t('Segmentations')}
|
|
||||||
segmentations={segmentations}
|
|
||||||
activeSegmentationId={selectedSegmentationId}
|
|
||||||
onClick={id => {
|
|
||||||
runCommand('setSegmentationActiveForToolGroups', {
|
|
||||||
segmentationId: id,
|
|
||||||
});
|
|
||||||
setSelectedSegmentationId(id);
|
|
||||||
}}
|
|
||||||
onToggleVisibility={id => {
|
|
||||||
segmentationService.toggleSegmentationVisibility(id);
|
|
||||||
}}
|
|
||||||
onToggleVisibilityAll={ids => {
|
|
||||||
ids.map(id => {
|
|
||||||
segmentationService.toggleSegmentationVisibility(id);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
onDelete={id => {
|
|
||||||
segmentationService.remove(id);
|
|
||||||
}}
|
|
||||||
onEdit={id => {
|
|
||||||
segmentationEditHandler({
|
|
||||||
id,
|
|
||||||
servicesManager,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{tmtvValue !== null ? (
|
|
||||||
<div className="bg-secondary-dark mt-4 flex items-baseline justify-between px-2 py-1">
|
|
||||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
|
||||||
{'TMTV:'}
|
|
||||||
</span>
|
|
||||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<ExportReports
|
|
||||||
segmentations={segmentations}
|
|
||||||
tmtvValue={tmtvValue}
|
|
||||||
config={config}
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="mt-auto mb-4 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
|
||||||
onClick={() => {
|
|
||||||
// navigate to a url in a new tab
|
|
||||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
width="15px"
|
|
||||||
height="15px"
|
|
||||||
name={'info'}
|
|
||||||
className={'text-primary-active ml-4 mr-3'}
|
|
||||||
/>
|
|
||||||
<span>{'User Guide'}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LegacyPanelRoiThresholdSegmentation.propTypes = {
|
|
||||||
commandsManager: PropTypes.shape({
|
|
||||||
runCommand: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
servicesManager: PropTypes.shape({
|
|
||||||
services: PropTypes.shape({
|
|
||||||
segmentationService: PropTypes.shape({
|
|
||||||
getSegmentation: PropTypes.func.isRequired,
|
|
||||||
getSegmentations: PropTypes.func.isRequired,
|
|
||||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
|
||||||
subscribe: PropTypes.func.isRequired,
|
|
||||||
EVENTS: PropTypes.object.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon, ActionButtons } from '@ohif/ui';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
export default function PanelRoiThresholdSegmentation({ servicesManager, commandsManager }) {
|
||||||
|
const { segmentationService } = servicesManager.services;
|
||||||
|
const { t } = useTranslation('PanelSUVExport');
|
||||||
|
|
||||||
|
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update UI based on segmentation changes (added, removed, updated)
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
// ~~ Subscription
|
||||||
|
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||||
|
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||||
|
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||||
|
const subscriptions = [];
|
||||||
|
|
||||||
|
[added, updated, removed].forEach(evt => {
|
||||||
|
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||||
|
const segmentations = segmentationService.getSegmentations();
|
||||||
|
setSegmentations(segmentations);
|
||||||
|
});
|
||||||
|
subscriptions.push(unsubscribe);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(unsub => {
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
||||||
|
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
||||||
|
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
label: 'Export CSV',
|
||||||
|
onClick: () => {
|
||||||
|
commandsManager.runCommand('exportTMTVReportCSV', {
|
||||||
|
segmentations,
|
||||||
|
tmtv: tmtvValue,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
disabled: tmtvValue === null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Create RT Report',
|
||||||
|
onClick: () => {
|
||||||
|
commandsManager.runCommand('createTMTVRTReport');
|
||||||
|
},
|
||||||
|
disabled: tmtvValue === null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mt-1 flex flex-col">
|
||||||
|
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
||||||
|
{tmtvValue !== null ? (
|
||||||
|
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
|
||||||
|
<span className="text-base font-bold uppercase tracking-widest text-white">
|
||||||
|
{'TMTV:'}
|
||||||
|
</span>
|
||||||
|
<div className="text-white">{`${tmtvValue} mL`}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="mt-1 flex justify-center">
|
||||||
|
<ActionButtons
|
||||||
|
actions={actions}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
||||||
|
onClick={() => {
|
||||||
|
// navigate to a url in a new tab
|
||||||
|
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
width="15px"
|
||||||
|
height="15px"
|
||||||
|
name={'info'}
|
||||||
|
className={'text-primary-active ml-4 mr-3'}
|
||||||
|
/>
|
||||||
|
<span>{'User Guide'}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelRoiThresholdSegmentation.propTypes = {
|
||||||
|
commandsManager: PropTypes.shape({
|
||||||
|
runCommand: PropTypes.func.isRequired,
|
||||||
|
}),
|
||||||
|
servicesManager: PropTypes.shape({
|
||||||
|
services: PropTypes.shape({
|
||||||
|
segmentationService: PropTypes.shape({
|
||||||
|
getSegmentation: PropTypes.func.isRequired,
|
||||||
|
getSegmentations: PropTypes.func.isRequired,
|
||||||
|
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||||
|
subscribe: PropTypes.func.isRequired,
|
||||||
|
EVENTS: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
@ -1,317 +0,0 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { SegmentationGroupTableExpanded, Icon } from '@ohif/ui';
|
|
||||||
import { createReportAsync } from '@ohif/extension-default';
|
|
||||||
|
|
||||||
import segmentationEditHandler from './segmentationEditHandler';
|
|
||||||
import ExportReports from './ExportReports';
|
|
||||||
import callInputDialog from './callInputDialog';
|
|
||||||
import callColorPickerDialog from './colorPickerDialog';
|
|
||||||
|
|
||||||
export default function PanelRoiThresholdSegmentation({
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
extensionManager,
|
|
||||||
}) {
|
|
||||||
const { segmentationService, viewportGridService, uiDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
|
||||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
|
||||||
|
|
||||||
const runCommand = useCallback(
|
|
||||||
(commandName, commandOptions = {}) => {
|
|
||||||
return commandsManager.runCommand(commandName, commandOptions);
|
|
||||||
},
|
|
||||||
[commandsManager]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update UI based on segmentation changes (added, removed, updated)
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
// ~~ Subscription
|
|
||||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
|
||||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
|
||||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
|
||||||
const subscriptions = [];
|
|
||||||
|
|
||||||
[added, updated, removed].forEach(evt => {
|
|
||||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
|
||||||
const segmentations = segmentationService.getSegmentations();
|
|
||||||
setSegmentations(segmentations);
|
|
||||||
});
|
|
||||||
subscriptions.push(unsubscribe);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscriptions.forEach(unsub => {
|
|
||||||
unsub();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSegmentationClick = (segmentationId: string) => {
|
|
||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
|
||||||
setSelectedSegmentationId(segmentationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationAdd = async () => {
|
|
||||||
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
|
||||||
setSelectedSegmentationId(segmentationId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentAdd = segmentationId => {
|
|
||||||
segmentationService.addSegment(segmentationId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getToolGroupIds = segmentationId => {
|
|
||||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
|
||||||
|
|
||||||
return toolGroupIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
|
||||||
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
|
||||||
|
|
||||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
||||||
|
|
||||||
toolGroupIds.forEach(toolGroupId => {
|
|
||||||
// const toolGroupId =
|
|
||||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
|
||||||
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const _setSegmentationConfiguration = useCallback(
|
|
||||||
(segmentationId, key, value) => {
|
|
||||||
segmentationService.setConfiguration({
|
|
||||||
segmentationId,
|
|
||||||
[key]: value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[segmentationService]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
const segmentInfo = segmentation.segments[segmentIndex];
|
|
||||||
const isVisible = !segmentInfo.isVisible;
|
|
||||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
||||||
toolGroupIds.forEach(toolGroupId => {
|
|
||||||
segmentationService.setSegmentVisibility(
|
|
||||||
segmentationId,
|
|
||||||
segmentIndex,
|
|
||||||
isVisible,
|
|
||||||
toolGroupId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
|
||||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentEdit = (segmentationId, segmentIndex) => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
|
|
||||||
const segment = segmentation.segments[segmentIndex];
|
|
||||||
const { label } = segment;
|
|
||||||
|
|
||||||
callInputDialog(uiDialogService, label, (label, actionId) => {
|
|
||||||
if (label === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
|
||||||
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
|
||||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
||||||
|
|
||||||
const segment = segmentation.segments[segmentIndex];
|
|
||||||
const { color, opacity } = segment;
|
|
||||||
|
|
||||||
const rgbaColor = {
|
|
||||||
r: color[0],
|
|
||||||
g: color[1],
|
|
||||||
b: color[2],
|
|
||||||
a: opacity / 255.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
|
||||||
if (actionId === 'cancel') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
|
|
||||||
newRgbaColor.r,
|
|
||||||
newRgbaColor.g,
|
|
||||||
newRgbaColor.b,
|
|
||||||
newRgbaColor.a * 255.0,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const storeSegmentation = async segmentationId => {
|
|
||||||
const datasources = extensionManager.getActiveDataSource();
|
|
||||||
|
|
||||||
const displaySetInstanceUIDs = await createReportAsync({
|
|
||||||
servicesManager,
|
|
||||||
getReport: () =>
|
|
||||||
commandsManager.runCommand('storeSegmentation', {
|
|
||||||
segmentationId,
|
|
||||||
dataSource: datasources[0],
|
|
||||||
}),
|
|
||||||
reportType: 'Segmentation',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the exported report in the active viewport as read only (similar to SR)
|
|
||||||
if (displaySetInstanceUIDs) {
|
|
||||||
// clear the segmentation that we exported, similar to the storeMeasurement
|
|
||||||
// where we remove the measurements and prompt again the user if they would like
|
|
||||||
// to re-read the measurements in a SR read only viewport
|
|
||||||
segmentationService.remove(segmentationId);
|
|
||||||
|
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
|
||||||
viewportId: viewportGridService.getActiveViewportId(),
|
|
||||||
displaySetInstanceUIDs,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationDownloadRTSS = segmentationId => {
|
|
||||||
commandsManager.runCommand('downloadRTSS', {
|
|
||||||
segmentationId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSegmentationDownload = segmentationId => {
|
|
||||||
commandsManager.runCommand('downloadSegmentation', {
|
|
||||||
segmentationId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
|
||||||
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="invisible-scrollbar overflow-y-auto overflow-x-hidden">
|
|
||||||
{/* show segmentation table */}
|
|
||||||
<div className="flex min-h-0 flex-col bg-black text-[13px] font-[300]">
|
|
||||||
<SegmentationGroupTableExpanded
|
|
||||||
disableEditing={false}
|
|
||||||
showAddSegmentation={true}
|
|
||||||
showAddSegment={false}
|
|
||||||
showDeleteSegment={true}
|
|
||||||
segmentations={segmentations}
|
|
||||||
onSegmentationAdd={onSegmentationAdd}
|
|
||||||
onSegmentationClick={onSegmentationClick}
|
|
||||||
onToggleSegmentationVisibility={id => {
|
|
||||||
segmentationService.toggleSegmentationVisibility(id);
|
|
||||||
}}
|
|
||||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
|
||||||
onSegmentationDelete={id => {
|
|
||||||
segmentationService.remove(id);
|
|
||||||
}}
|
|
||||||
onSegmentationEdit={id => {
|
|
||||||
segmentationEditHandler({
|
|
||||||
id,
|
|
||||||
servicesManager,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
segmentationConfig={{ initialConfig: segmentationService.getConfiguration() }}
|
|
||||||
onSegmentAdd={onSegmentAdd}
|
|
||||||
onSegmentClick={onSegmentClick}
|
|
||||||
onSegmentDelete={onSegmentDelete}
|
|
||||||
onSegmentEdit={onSegmentEdit}
|
|
||||||
onToggleSegmentLock={onToggleSegmentLock}
|
|
||||||
onSegmentColorClick={onSegmentColorClick}
|
|
||||||
storeSegmentation={storeSegmentation}
|
|
||||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
|
||||||
onSegmentationDownload={onSegmentationDownload}
|
|
||||||
setRenderOutline={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
|
||||||
}
|
|
||||||
setOutlineOpacityActive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
|
||||||
}
|
|
||||||
setRenderFill={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
|
||||||
}
|
|
||||||
setRenderInactiveSegmentations={value =>
|
|
||||||
_setSegmentationConfiguration(
|
|
||||||
selectedSegmentationId,
|
|
||||||
'renderInactiveSegmentations',
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setOutlineWidthActive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
|
||||||
}
|
|
||||||
setFillAlpha={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
|
||||||
}
|
|
||||||
setFillAlphaInactive={value =>
|
|
||||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{tmtvValue !== null ? (
|
|
||||||
<div className="bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1">
|
|
||||||
<span className="text-base font-bold uppercase tracking-widest text-white">
|
|
||||||
{'TMTV:'}
|
|
||||||
</span>
|
|
||||||
<div className="text-white">{`${tmtvValue} mL`}</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<ExportReports
|
|
||||||
segmentations={segmentations}
|
|
||||||
tmtvValue={tmtvValue}
|
|
||||||
config={config}
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80"
|
|
||||||
onClick={() => {
|
|
||||||
// navigate to a url in a new tab
|
|
||||||
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
width="15px"
|
|
||||||
height="15px"
|
|
||||||
name={'info'}
|
|
||||||
className={'text-primary-active ml-4 mr-3'}
|
|
||||||
/>
|
|
||||||
<span>{'User Guide'}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PanelRoiThresholdSegmentation.propTypes = {
|
|
||||||
commandsManager: PropTypes.shape({
|
|
||||||
runCommand: PropTypes.func.isRequired,
|
|
||||||
}),
|
|
||||||
servicesManager: PropTypes.shape({
|
|
||||||
services: PropTypes.shape({
|
|
||||||
segmentationService: PropTypes.shape({
|
|
||||||
getSegmentation: PropTypes.func.isRequired,
|
|
||||||
getSegmentations: PropTypes.func.isRequired,
|
|
||||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
|
||||||
subscribe: PropTypes.func.isRequired,
|
|
||||||
EVENTS: PropTypes.object.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
|
||||||
|
|
||||||
function callInputDialog(uiDialogService, label, callback) {
|
|
||||||
const dialogId = 'enter-segment-label';
|
|
||||||
|
|
||||||
const onSubmitHandler = ({ action, value }) => {
|
|
||||||
switch (action.id) {
|
|
||||||
case 'save':
|
|
||||||
callback(value.label, action.id);
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
callback('', action.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
uiDialogService.dismiss({ id: dialogId });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (uiDialogService) {
|
|
||||||
uiDialogService.create({
|
|
||||||
id: dialogId,
|
|
||||||
centralize: true,
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
content: Dialog,
|
|
||||||
contentProps: {
|
|
||||||
title: 'Segment',
|
|
||||||
value: { label },
|
|
||||||
noCloseButton: true,
|
|
||||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
|
||||||
actions: [
|
|
||||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
|
||||||
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
|
|
||||||
],
|
|
||||||
onSubmit: onSubmitHandler,
|
|
||||||
body: ({ value, setValue }) => {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
label="Enter the segment label"
|
|
||||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
|
||||||
autoFocus
|
|
||||||
className="border-primary-main bg-black"
|
|
||||||
type="text"
|
|
||||||
value={value.label}
|
|
||||||
onChange={event => {
|
|
||||||
event.persist();
|
|
||||||
setValue(value => ({ ...value, label: event.target.value }));
|
|
||||||
}}
|
|
||||||
onKeyPress={event => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
onSubmitHandler({ value, action: { id: 'save' } });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default callInputDialog;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
.chrome-picker {
|
|
||||||
background: #090c29 !important;
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Dialog } from '@ohif/ui';
|
|
||||||
import { ChromePicker } from 'react-color';
|
|
||||||
|
|
||||||
import './colorPickerDialog.css';
|
|
||||||
|
|
||||||
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
|
|
||||||
const dialogId = 'pick-color';
|
|
||||||
|
|
||||||
const onSubmitHandler = ({ action, value }) => {
|
|
||||||
switch (action.id) {
|
|
||||||
case 'save':
|
|
||||||
callback(value.rgbaColor, action.id);
|
|
||||||
break;
|
|
||||||
case 'cancel':
|
|
||||||
callback('', action.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
uiDialogService.dismiss({ id: dialogId });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (uiDialogService) {
|
|
||||||
uiDialogService.create({
|
|
||||||
id: dialogId,
|
|
||||||
centralize: true,
|
|
||||||
isDraggable: false,
|
|
||||||
showOverlay: true,
|
|
||||||
content: Dialog,
|
|
||||||
contentProps: {
|
|
||||||
title: 'Segment Color',
|
|
||||||
value: { rgbaColor },
|
|
||||||
noCloseButton: true,
|
|
||||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
|
||||||
actions: [
|
|
||||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
|
||||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
|
||||||
],
|
|
||||||
onSubmit: onSubmitHandler,
|
|
||||||
body: ({ value, setValue }) => {
|
|
||||||
const handleChange = color => {
|
|
||||||
setValue({ rgbaColor: color.rgb });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChromePicker
|
|
||||||
color={value.rgbaColor}
|
|
||||||
onChange={handleChange}
|
|
||||||
presetColors={[]}
|
|
||||||
width={300}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default callColorPickerDialog;
|
|
||||||
@ -1,3 +1,3 @@
|
|||||||
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
|
import PanelROIThresholdExport from './PanelROIThresholdExport';
|
||||||
|
|
||||||
export default PanelROIThresholdSegmentation;
|
export default PanelROIThresholdExport;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import PanelPetSUV from './PanelPetSUV';
|
import PanelPetSUV from './PanelPetSUV';
|
||||||
import PanelROIThresholdSegmentation from './PanelROIThresholdSegmentation';
|
import PanelROIThresholdExport from './PanelROIThresholdSegmentation';
|
||||||
|
|
||||||
export { PanelPetSUV, PanelROIThresholdSegmentation };
|
export { PanelPetSUV, PanelROIThresholdExport };
|
||||||
|
|||||||
@ -11,7 +11,10 @@ import createAndDownloadTMTVReport from './utils/createAndDownloadTMTVReport';
|
|||||||
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
|
import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructureSet';
|
||||||
|
|
||||||
const metadataProvider = classes.MetadataProvider;
|
const metadataProvider = classes.MetadataProvider;
|
||||||
const RECTANGLE_ROI_THRESHOLD_MANUAL = 'RectangleROIStartEndThreshold';
|
const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = [
|
||||||
|
'RectangleROIStartEndThreshold',
|
||||||
|
'RectangleROIThreshold',
|
||||||
|
];
|
||||||
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
|
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager, commandsManager, extensionManager }) => {
|
const commandsModule = ({ servicesManager, commandsManager, extensionManager }) => {
|
||||||
@ -52,6 +55,15 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
return toolGroupIds;
|
return toolGroupIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getAnnotationsSelectedByToolNames(toolNames) {
|
||||||
|
return toolNames.reduce((allAnnotationUIDs, toolName) => {
|
||||||
|
const annotationUIDs =
|
||||||
|
csTools.annotation.selection.getAnnotationsSelectedByToolName(toolName);
|
||||||
|
|
||||||
|
return allAnnotationUIDs.concat(annotationUIDs);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getMatchingPTDisplaySet: ({ viewportMatchDetails }) => {
|
getMatchingPTDisplaySet: ({ viewportMatchDetails }) => {
|
||||||
// Todo: this is assuming that the hanging protocol has successfully matched
|
// Todo: this is assuming that the hanging protocol has successfully matched
|
||||||
@ -108,7 +120,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
},
|
},
|
||||||
createNewLabelmapFromPT: async () => {
|
createNewLabelmapFromPT: async ({ label }) => {
|
||||||
// Create a segmentation of the same resolution as the source data
|
// Create a segmentation of the same resolution as the source data
|
||||||
// using volumeLoader.createAndCacheDerivedVolume.
|
// using volumeLoader.createAndCacheDerivedVolume.
|
||||||
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
const { viewportMatchDetails } = hangingProtocolService.getMatchDetails();
|
||||||
@ -173,20 +185,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
const { volumeId: segVolumeId } = representationData[LABELMAP];
|
const { volumeId: segVolumeId } = representationData[LABELMAP];
|
||||||
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
|
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
|
||||||
|
|
||||||
const labelmapVolume = cs.cache.getVolume(segmentationId);
|
const annotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||||
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
|
|
||||||
|
|
||||||
if (!referencedVolume) {
|
|
||||||
throw new Error('No Reference volume found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!labelmapVolume) {
|
|
||||||
throw new Error('No Reference labelmap found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
|
||||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (annotationUIDs.length === 0) {
|
if (annotationUIDs.length === 0) {
|
||||||
@ -198,6 +198,57 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const labelmapVolume = cs.cache.getVolume(segmentationId);
|
||||||
|
let referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||||
|
const ctReferencedVolume = cs.cache.getVolume(ctVolumeId);
|
||||||
|
|
||||||
|
// check if viewport is
|
||||||
|
|
||||||
|
if (!referencedVolume) {
|
||||||
|
throw new Error('No Reference volume found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!labelmapVolume) {
|
||||||
|
throw new Error('No Reference labelmap found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotation = csTools.annotation.state.getAnnotation(annotationUIDs[0]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
metadata: {
|
||||||
|
enabledElement: { viewport },
|
||||||
|
},
|
||||||
|
} = annotation;
|
||||||
|
|
||||||
|
const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId);
|
||||||
|
|
||||||
|
if (!showingReferenceVolume) {
|
||||||
|
// if the reference volume is not being displayed, we can't
|
||||||
|
// rely on it for thresholding, we have couple of options here
|
||||||
|
// 1. We choose whatever volume is being displayed
|
||||||
|
// 2. We check if it is a fusion viewport, we pick the volume
|
||||||
|
// that matches the size and dimensions of the labelmap. This might
|
||||||
|
// happen if the 4D PT is converted to a computed volume and displayed
|
||||||
|
// and wants to threshold the labelmap
|
||||||
|
// 3. We throw an error
|
||||||
|
const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
||||||
|
viewport.id
|
||||||
|
);
|
||||||
|
|
||||||
|
displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
|
||||||
|
const volume = cs.cache
|
||||||
|
.getVolumes()
|
||||||
|
.find(volume => volume.volumeId.includes(displaySetInstanceUID));
|
||||||
|
|
||||||
|
if (
|
||||||
|
cs.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) &&
|
||||||
|
cs.utilities.isEqual(volume.spacing, labelmapVolume.spacing)
|
||||||
|
) {
|
||||||
|
referencedVolume = volume;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
|
const { ptLower, ptUpper, ctLower, ctUpper } = getThresholdValues(
|
||||||
annotationUIDs,
|
annotationUIDs,
|
||||||
[referencedVolume, ctReferencedVolume],
|
[referencedVolume, ctReferencedVolume],
|
||||||
@ -218,8 +269,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
const { referencedVolumeId } = labelmap;
|
const { referencedVolumeId } = labelmap;
|
||||||
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
const referencedVolume = cs.cache.getVolume(referencedVolumeId);
|
||||||
|
|
||||||
const annotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
const annotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||||
);
|
);
|
||||||
|
|
||||||
const annotations = annotationUIDs.map(annotationUID =>
|
const annotations = annotationUIDs.map(annotationUID =>
|
||||||
@ -236,8 +287,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
},
|
},
|
||||||
getLesionStats: ({ labelmap, segmentIndex = 1 }) => {
|
getLesionStats: ({ labelmap, segmentIndex = 1 }) => {
|
||||||
const { scalarData, spacing } = labelmap;
|
const { scalarData, spacing } = labelmap;
|
||||||
|
const referencedScalarData = cs.cache.getVolume(labelmap.referencedVolumeId).getScalarData();
|
||||||
const { scalarData: referencedScalarData } = cs.cache.getVolume(labelmap.referencedVolumeId);
|
|
||||||
|
|
||||||
let segmentationMax = -Infinity;
|
let segmentationMax = -Infinity;
|
||||||
let segmentationMin = Infinity;
|
let segmentationMin = Infinity;
|
||||||
@ -287,19 +337,25 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
|
|
||||||
return calculateTMTV(labelmaps);
|
return calculateTMTV(labelmaps);
|
||||||
},
|
},
|
||||||
exportTMTVReportCSV: ({ segmentations, tmtv, config }) => {
|
exportTMTVReportCSV: ({ segmentations, tmtv, config, options }) => {
|
||||||
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
|
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
|
||||||
segmentations,
|
segmentations,
|
||||||
});
|
});
|
||||||
|
|
||||||
const tlg = actions.getTotalLesionGlycolysis({ segmentations });
|
const tlg = actions.getTotalLesionGlycolysis({ segmentations });
|
||||||
const additionalReportRows = [
|
const additionalReportRows = [
|
||||||
{ key: 'Total Metabolic Tumor Volume', value: { tmtv } },
|
|
||||||
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
|
{ key: 'Total Lesion Glycolysis', value: { tlg: tlg.toFixed(4) } },
|
||||||
{ key: 'Threshold Configuration', value: { ...config } },
|
{ key: 'Threshold Configuration', value: { ...config } },
|
||||||
];
|
];
|
||||||
|
|
||||||
createAndDownloadTMTVReport(segReport, additionalReportRows);
|
if (tmtv !== undefined) {
|
||||||
|
additionalReportRows.unshift({
|
||||||
|
key: 'Total Metabolic Tumor Volume',
|
||||||
|
value: { tmtv },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createAndDownloadTMTVReport(segReport, additionalReportRows, options);
|
||||||
},
|
},
|
||||||
getTotalLesionGlycolysis: ({ segmentations }) => {
|
getTotalLesionGlycolysis: ({ segmentations }) => {
|
||||||
const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
||||||
@ -323,9 +379,9 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ptVolume = cs.cache.getVolume(referencedVolumeId);
|
const ptVolume = cs.cache.getVolume(referencedVolumeId);
|
||||||
const mergedLabelData = mergedLabelmap.scalarData;
|
const mergedLabelData = mergedLabelmap.getScalarData();
|
||||||
|
|
||||||
if (mergedLabelData.length !== ptVolume.scalarData.length) {
|
if (mergedLabelData.length !== ptVolume.getScalarData().length) {
|
||||||
console.error(
|
console.error(
|
||||||
'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size'
|
'commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size'
|
||||||
);
|
);
|
||||||
@ -336,7 +392,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
for (let i = 0; i < mergedLabelData.length; i++) {
|
for (let i = 0; i < mergedLabelData.length; i++) {
|
||||||
// if not background
|
// if not background
|
||||||
if (mergedLabelData[i] !== 0) {
|
if (mergedLabelData[i] !== 0) {
|
||||||
suv += ptVolume.scalarData[i];
|
suv += ptVolume.getScalarData()[i];
|
||||||
totalLesionVoxelCount += 1;
|
totalLesionVoxelCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -351,8 +407,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
const { viewport } = _getActiveViewportsEnabledElement();
|
const { viewport } = _getActiveViewportsEnabledElement();
|
||||||
const { focalPoint, viewPlaneNormal } = viewport.getCamera();
|
const { focalPoint, viewPlaneNormal } = viewport.getCamera();
|
||||||
|
|
||||||
const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||||
);
|
);
|
||||||
|
|
||||||
const annotationUID = selectedAnnotationUIDs[0];
|
const annotationUID = selectedAnnotationUIDs[0];
|
||||||
@ -389,8 +445,8 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
setEndSliceForROIThresholdTool: () => {
|
setEndSliceForROIThresholdTool: () => {
|
||||||
const { viewport } = _getActiveViewportsEnabledElement();
|
const { viewport } = _getActiveViewportsEnabledElement();
|
||||||
|
|
||||||
const selectedAnnotationUIDs = csTools.annotation.selection.getAnnotationsSelectedByToolName(
|
const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(
|
||||||
RECTANGLE_ROI_THRESHOLD_MANUAL
|
RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS
|
||||||
);
|
);
|
||||||
|
|
||||||
const annotationUID = selectedAnnotationUIDs[0];
|
const annotationUID = selectedAnnotationUIDs[0];
|
||||||
@ -415,7 +471,11 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
|
|
||||||
Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => {
|
Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => {
|
||||||
const forAnnotations = stateManager.annotations[frameOfReferenceUID];
|
const forAnnotations = stateManager.annotations[frameOfReferenceUID];
|
||||||
const ROIAnnotations = forAnnotations[RECTANGLE_ROI_THRESHOLD_MANUAL];
|
const ROIAnnotations = RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce(
|
||||||
|
(annotations, toolName) => [...annotations, ...(forAnnotations[toolName] ?? [])],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
annotations.push(...ROIAnnotations);
|
annotations.push(...ROIAnnotations);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -483,7 +543,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager })
|
|||||||
|
|
||||||
report[id] = {
|
report[id] = {
|
||||||
...segReport,
|
...segReport,
|
||||||
PatientID: instance.PatientID,
|
PatientID: instance.PatientID ?? '000000',
|
||||||
PatientName: instance.PatientName.Alphabetic,
|
PatientName: instance.PatientName.Alphabetic,
|
||||||
StudyInstanceUID: instance.StudyInstanceUID,
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PanelPetSUV, PanelROIThresholdSegmentation } from './Panels';
|
import { PanelPetSUV, PanelROIThresholdExport } from './Panels';
|
||||||
import { Toolbox } from '@ohif/ui';
|
import { Toolbox } from '@ohif/ui';
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
@ -17,20 +17,26 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrappedROIThresholdSeg = () => {
|
const wrappedROIThresholdToolbox = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbox
|
<Toolbox
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
buttonSectionId="tmtvToolbox"
|
buttonSectionId="ROIThresholdToolbox"
|
||||||
title="Threshold Tools"
|
title="Threshold Tools"
|
||||||
/>
|
/>
|
||||||
<PanelROIThresholdSegmentation
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappedROIThresholdExport = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PanelROIThresholdExport
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
extensionManager={extensionManager}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -45,11 +51,18 @@ function getPanelModule({ commandsManager, extensionManager, servicesManager })
|
|||||||
component: wrappedPanelPetSuv,
|
component: wrappedPanelPetSuv,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ROIThresholdSeg',
|
name: 'tmtvBox',
|
||||||
iconName: 'tab-segmentation',
|
iconName: 'tab-segmentation',
|
||||||
iconLabel: 'Segmentation',
|
iconLabel: 'Segmentation',
|
||||||
label: 'Segmentation',
|
label: 'Segmentation Toolbox',
|
||||||
component: wrappedROIThresholdSeg,
|
component: wrappedROIThresholdToolbox,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tmtvExport',
|
||||||
|
iconName: 'tab-segmentation',
|
||||||
|
iconLabel: 'Segmentation',
|
||||||
|
label: 'Segmentation Export',
|
||||||
|
component: wrappedROIThresholdExport,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,17 +40,19 @@ function calculateSuvPeak(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (labelmap.scalarData.length !== referenceVolume.scalarData.length) {
|
const labelmapData = labelmap.getScalarData();
|
||||||
|
const referenceVolumeData = referenceVolume.getScalarData();
|
||||||
|
|
||||||
|
if (labelmapData.length !== referenceVolumeData.length) {
|
||||||
throw new Error('labelmap and referenceVolume must have the same number of pixels');
|
throw new Error('labelmap and referenceVolume must have the same number of pixels');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { scalarData: labelmapData, dimensions, imageData: labelmapImageData } = labelmap;
|
const { dimensions, imageData: labelmapImageData } = labelmap;
|
||||||
|
const { imageData: referenceVolumeImageData } = referenceVolume;
|
||||||
const { scalarData: referenceVolumeData, imageData: referenceVolumeImageData } = referenceVolume;
|
|
||||||
|
|
||||||
let boundsIJK;
|
let boundsIJK;
|
||||||
// Todo: using the first annotation for now
|
// Todo: using the first annotation for now
|
||||||
if (annotations && annotations[0].data?.cachedStats) {
|
if (annotations?.length && annotations[0].data?.cachedStats) {
|
||||||
const { projectionPoints } = annotations[0].data.cachedStats;
|
const { projectionPoints } = annotations[0].data.cachedStats;
|
||||||
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
|
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export default function createAndDownloadTMTVReport(segReport, additionalReportRows) {
|
export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) {
|
||||||
const firstReport = segReport[Object.keys(segReport)[0]];
|
const firstReport = segReport[Object.keys(segReport)[0]];
|
||||||
const columns = Object.keys(firstReport);
|
const columns = Object.keys(firstReport);
|
||||||
const csv = [columns.join(',')];
|
const csv = [columns.join(',')];
|
||||||
@ -40,6 +40,6 @@ export default function createAndDownloadTMTVReport(segReport, additionalReportR
|
|||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `${firstReport.PatientID}_tmtv.csv`;
|
a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,7 +136,7 @@ function modeFactory() {
|
|||||||
props: {
|
props: {
|
||||||
leftPanels: [tracked.thumbnailList],
|
leftPanels: [tracked.thumbnailList],
|
||||||
rightPanels: [dicomSeg.panel, tracked.measurements],
|
rightPanels: [dicomSeg.panel, tracked.measurements],
|
||||||
// rightPanelDefaultClosed: true, // optional prop to start with collapse panels
|
// rightPanelClosed: true, // optional prop to start with collapse panels
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
namespace: tracked.viewport,
|
namespace: tracked.viewport,
|
||||||
|
|||||||
@ -11,6 +11,13 @@ const ReferenceLinesListeners: RunCommand = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const toggleEnabledDisabledToolbar = {
|
||||||
|
commandName: 'toggleEnabledDisabledToolbar',
|
||||||
|
commandOptions: {
|
||||||
|
toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const moreTools = [
|
const moreTools = [
|
||||||
{
|
{
|
||||||
id: 'MoreTools',
|
id: 'MoreTools',
|
||||||
@ -80,13 +87,7 @@ const moreTools = [
|
|||||||
icon: 'tool-referenceLines',
|
icon: 'tool-referenceLines',
|
||||||
label: 'Reference Lines',
|
label: 'Reference Lines',
|
||||||
tooltip: 'Show Reference Lines',
|
tooltip: 'Show Reference Lines',
|
||||||
commands: {
|
commands: toggleEnabledDisabledToolbar,
|
||||||
commandName: 'setToolEnabled',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'ReferenceLines',
|
|
||||||
toggle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
listeners: {
|
listeners: {
|
||||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||||
@ -94,17 +95,11 @@ const moreTools = [
|
|||||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||||
}),
|
}),
|
||||||
createButton({
|
createButton({
|
||||||
id: 'ImageOverlay',
|
id: 'ImageOverlayViewer',
|
||||||
icon: 'toggle-dicom-overlay',
|
icon: 'toggle-dicom-overlay',
|
||||||
label: 'Image Overlay',
|
label: 'Image Overlay',
|
||||||
tooltip: 'Toggle Image Overlay',
|
tooltip: 'Toggle Image Overlay',
|
||||||
commands: {
|
commands: toggleEnabledDisabledToolbar,
|
||||||
commandName: 'setToolEnabled',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'ImageOverlayViewer',
|
|
||||||
toggle: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||||
}),
|
}),
|
||||||
createButton({
|
createButton({
|
||||||
|
|||||||
@ -108,8 +108,8 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
|
|
||||||
customizationService.addModeCustomizations([
|
customizationService.addModeCustomizations([
|
||||||
{
|
{
|
||||||
id: 'segmentation.disableEditing',
|
id: 'segmentation.panel',
|
||||||
value: true,
|
disableEditing: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
props: {
|
props: {
|
||||||
leftPanels: [tracked.thumbnailList],
|
leftPanels: [tracked.thumbnailList],
|
||||||
rightPanels: [dicomSeg.panel, tracked.measurements],
|
rightPanels: [dicomSeg.panel, tracked.measurements],
|
||||||
rightPanelDefaultClosed: true,
|
rightPanelClosed: true,
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
namespace: tracked.viewport,
|
namespace: tracked.viewport,
|
||||||
|
|||||||
@ -64,7 +64,12 @@ function initDefaultToolGroup(
|
|||||||
{ toolName: toolNames.Magnify },
|
{ toolName: toolNames.Magnify },
|
||||||
{ toolName: toolNames.SegmentationDisplay },
|
{ toolName: toolNames.SegmentationDisplay },
|
||||||
{ toolName: toolNames.CalibrationLine },
|
{ toolName: toolNames.CalibrationLine },
|
||||||
{ toolName: toolNames.AdvancedMagnify },
|
{
|
||||||
|
toolName: toolNames.AdvancedMagnify,
|
||||||
|
configuration: {
|
||||||
|
disableOnPassive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{ toolName: toolNames.UltrasoundDirectional },
|
{ toolName: toolNames.UltrasoundDirectional },
|
||||||
{ toolName: toolNames.PlanarFreehandROI },
|
{ toolName: toolNames.PlanarFreehandROI },
|
||||||
{ toolName: toolNames.SplineROI },
|
{ toolName: toolNames.SplineROI },
|
||||||
|
|||||||
@ -11,6 +11,20 @@ const ReferenceLinesListeners: RunCommand = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const toggleEnabledDisabledToolbar = {
|
||||||
|
commandName: 'toggleEnabledDisabledToolbar',
|
||||||
|
commandOptions: {
|
||||||
|
toolGroupIds: ['default', 'mpr', 'SRToolGroup', 'volume3d'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleActiveDisabledToolbar = {
|
||||||
|
commandName: 'toggleActiveDisabledToolbar',
|
||||||
|
commandOptions: {
|
||||||
|
toolGroupIds: ['default', 'mpr', 'SRToolGroup'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const moreTools = [
|
const moreTools = [
|
||||||
{
|
{
|
||||||
id: 'MoreTools',
|
id: 'MoreTools',
|
||||||
@ -80,13 +94,7 @@ const moreTools = [
|
|||||||
icon: 'tool-referenceLines',
|
icon: 'tool-referenceLines',
|
||||||
label: 'Reference Lines',
|
label: 'Reference Lines',
|
||||||
tooltip: 'Show Reference Lines',
|
tooltip: 'Show Reference Lines',
|
||||||
commands: {
|
commands: toggleEnabledDisabledToolbar,
|
||||||
commandName: 'setToolEnabled',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'ReferenceLines',
|
|
||||||
toggle: true, // Toggle the tool on/off upon click
|
|
||||||
},
|
|
||||||
},
|
|
||||||
listeners: {
|
listeners: {
|
||||||
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
|
||||||
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
|
||||||
@ -94,17 +102,11 @@ const moreTools = [
|
|||||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||||
}),
|
}),
|
||||||
createButton({
|
createButton({
|
||||||
id: 'ImageOverlay',
|
id: 'ImageOverlayViewer',
|
||||||
icon: 'toggle-dicom-overlay',
|
icon: 'toggle-dicom-overlay',
|
||||||
label: 'Image Overlay',
|
label: 'Image Overlay',
|
||||||
tooltip: 'Toggle Image Overlay',
|
tooltip: 'Toggle Image Overlay',
|
||||||
commands: {
|
commands: toggleEnabledDisabledToolbar,
|
||||||
commandName: 'setToolEnabled',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'ImageOverlayViewer',
|
|
||||||
toggle: true, // Toggle the tool on/off upon click
|
|
||||||
},
|
|
||||||
},
|
|
||||||
evaluate: 'evaluate.cornerstoneTool.toggle',
|
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||||
}),
|
}),
|
||||||
createButton({
|
createButton({
|
||||||
@ -175,8 +177,8 @@ const moreTools = [
|
|||||||
icon: 'icon-tool-loupe',
|
icon: 'icon-tool-loupe',
|
||||||
label: 'Loupe',
|
label: 'Loupe',
|
||||||
tooltip: 'Loupe',
|
tooltip: 'Loupe',
|
||||||
commands: setToolActiveToolbar,
|
commands: toggleActiveDisabledToolbar,
|
||||||
evaluate: 'evaluate.cornerstoneTool',
|
evaluate: 'evaluate.cornerstoneTool.toggle',
|
||||||
}),
|
}),
|
||||||
createButton({
|
createButton({
|
||||||
id: 'UltrasoundDirectionalTool',
|
id: 'UltrasoundDirectionalTool',
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
// TODO: torn, can either bake this here; or have to create a whole new button type
|
// TODO: torn, can either bake this here; or have to create a whole new button type
|
||||||
// Only ways that you can pass in a custom React component for render :l
|
// Only ways that you can pass in a custom React component for render :l
|
||||||
import { WindowLevelMenuItem } from '@ohif/ui';
|
|
||||||
import { defaults, ToolbarService } from '@ohif/core';
|
import { defaults, ToolbarService } from '@ohif/core';
|
||||||
import type { Button } from '@ohif/core/types';
|
import type { Button } from '@ohif/core/types';
|
||||||
|
|
||||||
const { windowLevelPresets } = defaults;
|
|
||||||
const { createButton } = ToolbarService;
|
const { createButton } = ToolbarService;
|
||||||
|
|
||||||
export const setToolActiveToolbar = {
|
export const setToolActiveToolbar = {
|
||||||
|
|||||||
@ -87,8 +87,8 @@ function modeFactory({ modeConfiguration }) {
|
|||||||
id: ohif.layout,
|
id: ohif.layout,
|
||||||
props: {
|
props: {
|
||||||
leftPanels: [ohif.leftPanel],
|
leftPanels: [ohif.leftPanel],
|
||||||
leftPanelDefaultClosed: true, // we have problem with rendering thumbnails for microscopy images
|
leftPanelClosed: true, // we have problem with rendering thumbnails for microscopy images
|
||||||
rightPanelDefaultClosed: true, // we do not have the save microscopy measurements yet
|
rightPanelClosed: true, // we do not have the save microscopy measurements yet
|
||||||
rightPanels: ['@ohif/extension-dicom-microscopy.panelModule.measure'],
|
rightPanels: ['@ohif/extension-dicom-microscopy.panelModule.measure'],
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
|
|||||||
12
modes/preclinical-4d/.webpack/webpack.dev.js
Normal file
12
modes/preclinical-4d/.webpack/webpack.dev.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
const ENTRY = {
|
||||||
|
app: `${SRC_DIR}/index.tsx`,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user