feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)
* feat: add initial sop class handler for SEG * feat: move segmentation service * update segmentation service methods * fix: viewport data structure * feat: Add initial render for the DICOM SEG for each displaySet * fix: various wrong architectural dependencies between services * feat: initial separate SEG display in each viewport * feat: refactore viewport action bar * initial work for SEG hydration * fix: various bugs regarding drag and dropping different viewports * fix: rendering issues for multiple seg displaysets * fix: bugs for thumbnail and hydration * feat: fix the initial segment color * update after rebase * feat: initial design for the segmentation group table * feat: initial new design for the segmentation panel * feat: segmentation panel * feat: make segmentation appear on all related viewports * fix: segmentation load bug based on functional groups * initial work for segmentation crosshairs * fix: various stylings and functionality * fix: various stylings for the seg panel * fix: overflow styles * feat: add more ui components * feat: added segmentation config * feat: add jump to segment * feat: add jump to segment for DICOM SEG viewport * fix: bugs after rebase * feat: jump in segmentation viewport * fix: mpr support for seg * feat: add more icons * feat: new icons * feat: add new side panel * feat: add segmentation config * feat: add loading indicator to ohif * feat: make hanging protocols follow matching rules for viewports * feat: enhance drag and drop to be hanging protocol aware * fix: mpr restore previous layout * fix: crosshairs toggle * fix: add auth headers to the dicom loader via dicomwebclient * fix: bug for crosshairs toggle in mpr * fix: seg viewport reusing old toolGroup * feat: add loading animation with lottie * fix: various bugs for Segmentation hydration in mpr * feat: change outline alpha to outline opacity * feat: loading indicator for seg viewport * fix: various segmentation group styles * fix: local mode for seg * feat: add animation for the highlight * fix: loading indicatro to show segment indices * feat: enhance modality drop down ui * fix: panels * fix: download form * fix: layout shift in loading indicator * fix: loading indicator to have correct values * fix: update software number * fix: image jump between MPR and default * fix: segmentation cleanup and cine service cleanup * fix: segmentation toolgroup clena up * fix: highlight interval should not trigger again * fix: issue with multiframe sorting * rename: change onSeriesChange to onArrowsClick * fix: buttons for tmtv and layout shift * fix: various bugs wrt crosshairs * fix: crosshairs re init on reset camera * fix: middle slice calculation different from cs middle reset camera * fix: reset camera should reset viewport camera * fix: loading segments in MPR mode * fix: tmtv hp back to before * fix: layout shift * feat: orientation markers for volume viewport * fix: capture for volume viewports * fix: memory leak for back to worklist * fix: loading bar bg color * fix: side panel for only one panel * fix: react select style in production * fix: various styling for segmentation groups * fix: various ui styles * feat: add new segmentation config styles * fix: hover state for segmentation item * fix: side panel layout shift * temp add panels * try to fix scrollbar for thimbnail * fix: scroll not appearing for side panels * feat: changed styles for scrollbar * feat: add cpu fallback warning * fix: webworker destroy * fix: select styles * fix: orientation marker color and position * fix: capture screenshot for volume viewports * fix: hide segment visibility on mpr * fix: reloading an already loaded seg displayset * feat: add is equal * fix: mpr jump to measurements * apply review comments * fix: optimization for the segmentation load * fix: remove unnecessary context menu and hp service reset * fix: segmentation service wrt brush settings * feat: initial work for the stack synchronization * fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements * fix: various react proptypes * fix: thumbnails for the tmtv and change default props to default params * feat: make showing loading indicator configurable and add docs * bump versions and add more docs * fix demo * update cornerstone versions * apply review comments * feat: add reference lines * fix build * fix unit tests * add reference lines icon * fix: e2e tests * fix static wado config * docs: add segmentation service docs * fix: docker build * fix: keep camera and bump versions * Try re-enabling minification to fix deploy previews Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
This commit is contained in:
parent
08c58b0634
commit
83ea228d86
43
.docker/README.md
Normal file
43
.docker/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Docker compose files
|
||||
|
||||
This folder contains docker-compose files used to spin up OHIF-Viewer with
|
||||
different options such as locally or with any PAS you desire to
|
||||
|
||||
## Public Server
|
||||
|
||||
## Local Orthanc
|
||||
|
||||
### Build
|
||||
|
||||
`$ docker-compose -f docker-compose-orthanc.yml build`
|
||||
|
||||
### Run
|
||||
|
||||
Starts containers and leaves them running in the background.
|
||||
|
||||
`$ docker-compose -f docker-compose-orthanc.yml up -d`
|
||||
|
||||
then, access the application at [http://localhost](http://localhost)
|
||||
|
||||
**remember that you have to access orthanc application and include your studies
|
||||
there**
|
||||
|
||||
## Local Dcm4chee
|
||||
|
||||
#### build
|
||||
|
||||
`$ docker-compose -f docker-compose-dcm4chee.yml build`
|
||||
|
||||
#### run
|
||||
|
||||
`$ docker-compose -f docker-compose-dcm4chee.yml up -d`
|
||||
|
||||
then, access the application at [http://localhost](http://localhost)
|
||||
|
||||
**remember that you have to access dcm4chee application and include your studies
|
||||
there** You can use the following command to import your studies into dcm4che
|
||||
|
||||
`$ docker run -v {YOUR_STUDY_FOLDER}:/tmp --rm --network=docker_dcm4che_default dcm4che/dcm4che-tools:5.14.0 storescu -cDCM4CHEE@arc:11112 /tmp`
|
||||
|
||||
**make sure that your Docker network name is docker_dcm4chee_default or change
|
||||
it to the right one**
|
||||
@ -1,5 +1,6 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen ${PORT};
|
||||
# listen 3000;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
@ -1,4 +1,6 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
envsubst `${PORT}` < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
then
|
||||
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Set the default behavior,
|
||||
# in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
# Declares that files will always have CRLF line ends
|
||||
*.sh text eol=lf
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -40,3 +40,5 @@ screenshots/
|
||||
# autogenerated files
|
||||
platform/viewer/src/pluginImports.js
|
||||
/Viewers.iml
|
||||
platform/viewer/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
|
||||
platform/viewer/.recipes/OpenResty-Orthanc/logs/*
|
||||
|
||||
39
Dockerfile
39
Dockerfile
@ -1,3 +1,23 @@
|
||||
# This dockerfile is used to publish the `ohif/viewer` image on dockerhub.
|
||||
#
|
||||
# It's a good example of how to build our static application and package it
|
||||
# with a web server capable of hosting it as static content.
|
||||
#
|
||||
# docker build
|
||||
# --------------
|
||||
# If you would like to use this dockerfile to build and tag an image, make sure
|
||||
# you set the context to the project's root directory:
|
||||
# https://docs.docker.com/engine/reference/commandline/build/
|
||||
#
|
||||
#
|
||||
# SUMMARY
|
||||
# --------------
|
||||
# This dockerfile has two stages:
|
||||
#
|
||||
# 1. Building the React application for production
|
||||
# 2. Setting up our Nginx (Alpine Linux) image w/ step one's output
|
||||
#
|
||||
|
||||
|
||||
# Stage 1: Build the application
|
||||
# docker build -t ohif/viewer:latest .
|
||||
@ -34,10 +54,21 @@ RUN yarn install --frozen-lockfile --verbose
|
||||
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
ENV QUICK_BUILD true
|
||||
# ENV GENERATE_SOURCEMAP=false
|
||||
# ENV REACT_APP_CONFIG=config/default.js
|
||||
|
||||
RUN yarn run build
|
||||
|
||||
# Stage 3: Bundle the built application into a Docker container
|
||||
# which runs Nginx using Alpine Linux
|
||||
FROM nginx:1.15.5-alpine as final
|
||||
RUN apk add --no-cache bash
|
||||
RUN rm -rf /etc/nginx/conf.d
|
||||
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
|
||||
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final
|
||||
#RUN apk add --no-cache bash
|
||||
ENV PORT=3000
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
USER nginx
|
||||
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
|
||||
RUN envsubst `${PORT}` < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
RUN chmod 777 /usr/src/entrypoint.sh
|
||||
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
|
||||
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
8
extensions/cornerstone-dicom-seg/.webpack/webpack.dev.js
Normal file
8
extensions/cornerstone-dicom-seg/.webpack/webpack.dev.js
Normal file
@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
63
extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js
Normal file
63
extensions/cornerstone-dicom-seg/.webpack/webpack.prod.js
Normal file
@ -0,0 +1,63 @@
|
||||
const path = require('path');
|
||||
const pkg = require('../package.json');
|
||||
|
||||
const outputFile = 'index.umd.js';
|
||||
const rootDir = path.resolve(__dirname, '../');
|
||||
const outputFolder = path.join(__dirname, `../dist/umd/${pkg.name}/`);
|
||||
|
||||
// Todo: add ESM build for the extension in addition to umd build
|
||||
|
||||
const config = {
|
||||
mode: 'production',
|
||||
entry: rootDir + '/' + pkg.module,
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
path: outputFolder,
|
||||
filename: outputFile,
|
||||
library: pkg.name,
|
||||
libraryTarget: 'umd',
|
||||
chunkFilename: '[name].chunk.js',
|
||||
umdNamedDefine: true,
|
||||
globalObject: "typeof self !== 'undefined' ? self : this",
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
react: {
|
||||
root: 'React',
|
||||
commonjs2: 'react',
|
||||
commonjs: 'react',
|
||||
amd: 'react',
|
||||
},
|
||||
'@ohif/core': {
|
||||
commonjs2: '@ohif/core',
|
||||
commonjs: '@ohif/core',
|
||||
amd: '@ohif/core',
|
||||
root: '@ohif/core',
|
||||
},
|
||||
'@ohif/ui': {
|
||||
commonjs2: '@ohif/ui',
|
||||
commonjs: '@ohif/ui',
|
||||
amd: '@ohif/ui',
|
||||
root: '@ohif/ui',
|
||||
},
|
||||
},
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /(\.jsx|\.js|\.tsx|\.ts)$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve('./node_modules'), path.resolve('./src')],
|
||||
extensions: ['.json', '.js', '.jsx', '.tsx', '.ts'],
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
20
extensions/cornerstone-dicom-seg/LICENSE
Normal file
20
extensions/cornerstone-dicom-seg/LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
18
extensions/cornerstone-dicom-seg/README.md
Normal file
18
extensions/cornerstone-dicom-seg/README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# dicom-seg
|
||||
## Description
|
||||
|
||||
DICOM SEG read workflow. This extension will allow you to load a DICOM SEG image
|
||||
and display it on OHIF. Currently Segmentations are loaded as a volumetric labelmap
|
||||
and displayed as a 3D volume.
|
||||
|
||||
This extension provides a SEG viewport, which enables rendering and reviewing
|
||||
of the DICOM SEG images. However, in order to fully load all the segments
|
||||
you will need to click on the SEG Pill button on the viewport action bar
|
||||
to fully load the segments.
|
||||
|
||||
## Author
|
||||
|
||||
OHIF
|
||||
|
||||
## License
|
||||
MIT
|
||||
44
extensions/cornerstone-dicom-seg/babel.config.js
Normal file
44
extensions/cornerstone-dicom-seg/babel.config.js
Normal file
@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
plugins: ['inline-react-svg', '@babel/plugin-proposal-class-properties'],
|
||||
env: {
|
||||
test: {
|
||||
presets: [
|
||||
[
|
||||
// TODO: https://babeljs.io/blog/2019/03/19/7.4.0#migration-from-core-js-2
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: 'commonjs',
|
||||
debug: false,
|
||||
},
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/plugin-transform-runtime',
|
||||
],
|
||||
},
|
||||
production: {
|
||||
presets: [
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
development: {
|
||||
presets: [
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
},
|
||||
};
|
||||
71
extensions/cornerstone-dicom-seg/package.json
Normal file
71
extensions/cornerstone-dicom-seg/package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.0.0",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"main": "dist/umd/@ohif/dicom-seg/index.umd.js",
|
||||
"module": "src/index.tsx",
|
||||
"files": [
|
||||
"dist/**",
|
||||
"public/**",
|
||||
"README.md"
|
||||
],
|
||||
"repository": "OHIF/Viewers",
|
||||
"keywords": [
|
||||
"ohif-extension"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:dicom-seg": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/extension-default": "^3.0.0",
|
||||
"@ohif/extension-cornerstone": "^3.0.0",
|
||||
"@ohif/i18n": "^1.0.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^10.11.0",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"webpack": "^5.50.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
"react-color": "^2.19.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-arrow-functions": "^7.2.0",
|
||||
"@babel/plugin-transform-regenerator": "^7.4.5",
|
||||
"@babel/plugin-transform-runtime": "^7.5.0",
|
||||
"babel-plugin-inline-react-svg": "^2.0.1",
|
||||
"@babel/preset-env": "^7.5.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-loader": "^8.0.0-beta.4",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^10.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^14.1.0",
|
||||
"eslint": "^5.0.1",
|
||||
"eslint-loader": "^2.0.0",
|
||||
"webpack": "^5.50.0",
|
||||
"webpack-merge": "^5.7.3",
|
||||
"webpack-cli": "^4.7.2"
|
||||
}
|
||||
}
|
||||
312
extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js
Normal file
312
extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js
Normal file
@ -0,0 +1,312 @@
|
||||
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
|
||||
|
||||
let loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPClassUID,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
wadoUriRoot,
|
||||
} = instance;
|
||||
|
||||
const displaySet = {
|
||||
Modality: 'SEG',
|
||||
loading: false,
|
||||
isReconstructable: true, // by default for now since it is a volumetric SEG currently
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
referencedImages: null,
|
||||
referencedSeriesInstanceUID: null,
|
||||
referencedDisplaySetInstanceUID: null,
|
||||
isDerivedDisplaySet: true,
|
||||
isLoaded: false,
|
||||
isHydrated: false,
|
||||
segments: {},
|
||||
sopClassUids,
|
||||
instance,
|
||||
wadoRoot,
|
||||
wadoUriRoot,
|
||||
wadoUri,
|
||||
};
|
||||
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
|
||||
if (!referencedSeriesSequence) {
|
||||
throw new Error('ReferencedSeriesSequence is missing for the SEG');
|
||||
}
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0];
|
||||
|
||||
displaySet.referencedImages =
|
||||
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
if (!referencedDisplaySets || referencedDisplaySets.length === 0) {
|
||||
throw new Error('Referenced DisplaySet is missing for the SEG');
|
||||
}
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID =
|
||||
referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
|
||||
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||
const referencedVolumeId = `cornerstoneStreamingImageVolume:${displaySet.referencedVolumeURI}`;
|
||||
displaySet.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
|
||||
displaySet.load = async ({ headers }) =>
|
||||
await _load(displaySet, servicesManager, extensionManager, headers);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
const { SOPInstanceUID } = segDisplaySet;
|
||||
if (
|
||||
(segDisplaySet.loading || segDisplaySet.isLoaded) &&
|
||||
loadPromises[SOPInstanceUID]
|
||||
) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
segDisplaySet.loading = true;
|
||||
|
||||
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||
// and also return the same promise to any other callers.
|
||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
const { SegmentationService } = servicesManager.services;
|
||||
|
||||
if (_segmentationExistsInCache(segDisplaySet, SegmentationService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!segDisplaySet.segments ||
|
||||
Object.keys(segDisplaySet.segments).length === 0
|
||||
) {
|
||||
const segments = await _loadSegments(
|
||||
extensionManager,
|
||||
segDisplaySet,
|
||||
headers
|
||||
);
|
||||
|
||||
segDisplaySet.segments = segments;
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
SegmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
null,
|
||||
suppressEvents
|
||||
)
|
||||
.then(() => {
|
||||
segDisplaySet.loading = false;
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
segDisplaySet.loading = false;
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
async function _loadSegments(extensionManager, segDisplaySet, headers) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
null,
|
||||
headers
|
||||
);
|
||||
|
||||
const dicomData = DicomMessage.readFile(segArrayBuffer);
|
||||
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
if (!Array.isArray(dataset.SegmentSequence)) {
|
||||
dataset.SegmentSequence = [dataset.SegmentSequence];
|
||||
}
|
||||
|
||||
const segments = _getSegments(dataset);
|
||||
return segments;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(segDisplaySet, SegmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
const labelmapVolumeId = segDisplaySet.displaySetInstanceUID;
|
||||
const segVolume = SegmentationService.getLabelmapVolume(labelmapVolumeId);
|
||||
|
||||
return segVolume !== undefined;
|
||||
}
|
||||
|
||||
function _getPixelData(dataset, segments) {
|
||||
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
|
||||
let nextOffset = 0;
|
||||
|
||||
Object.keys(segments).forEach(segmentKey => {
|
||||
const segment = segments[segmentKey];
|
||||
segment.numberOfFrames = segment.functionalGroups.length;
|
||||
segment.size = segment.numberOfFrames * frameSize;
|
||||
segment.offset = nextOffset;
|
||||
nextOffset = segment.offset + segment.size;
|
||||
const packedSegment = dataset.PixelData[0].slice(
|
||||
segment.offset,
|
||||
nextOffset
|
||||
);
|
||||
|
||||
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
|
||||
segment.geometry = geometryFromFunctionalGroups(
|
||||
dataset,
|
||||
segment.functionalGroups
|
||||
);
|
||||
});
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function geometryFromFunctionalGroups(dataset, perFrame) {
|
||||
let pixelMeasures =
|
||||
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
|
||||
let planeOrientation =
|
||||
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
|
||||
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
|
||||
|
||||
const geometry = {};
|
||||
|
||||
// NB: DICOM PixelSpacing is defined as Row then Column,
|
||||
// unlike ImageOrientationPatient
|
||||
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
|
||||
if (!spacingBetweenSlices) {
|
||||
if (pixelMeasures.SliceThickness) {
|
||||
console.log('Using SliceThickness as SpacingBetweenSlices');
|
||||
spacingBetweenSlices = pixelMeasures.SliceThickness;
|
||||
}
|
||||
}
|
||||
geometry.spacing = [
|
||||
pixelMeasures.PixelSpacing[1],
|
||||
pixelMeasures.PixelSpacing[0],
|
||||
spacingBetweenSlices,
|
||||
].map(Number);
|
||||
|
||||
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
|
||||
Number
|
||||
);
|
||||
|
||||
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
|
||||
const columnStepToPatient = orientation.slice(0, 3);
|
||||
const rowStepToPatient = orientation.slice(3, 6);
|
||||
geometry.planeNormal = [];
|
||||
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
|
||||
|
||||
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
|
||||
Number
|
||||
);
|
||||
let lastPosition = perFrame[
|
||||
perFrame.length - 1
|
||||
].PlanePositionSequence.ImagePositionPatient.map(Number);
|
||||
geometry.sliceStep = [];
|
||||
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
|
||||
vtkMath.normalize(geometry.sliceStep);
|
||||
geometry.direction = columnStepToPatient
|
||||
.concat(rowStepToPatient)
|
||||
.concat(geometry.sliceStep);
|
||||
geometry.origin = planePosition.ImagePositionPatient.map(Number);
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function _getSegments(dataset) {
|
||||
const segments = {};
|
||||
|
||||
dataset.SegmentSequence.forEach(segment => {
|
||||
const cielab = segment.RecommendedDisplayCIELabValue;
|
||||
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
|
||||
Math.round(x * 255)
|
||||
);
|
||||
|
||||
rgba.push(255);
|
||||
const segmentNumber = segment.SegmentNumber;
|
||||
|
||||
segments[segmentNumber] = {
|
||||
color: rgba,
|
||||
functionalGroups: [],
|
||||
offset: null,
|
||||
size: null,
|
||||
pixelData: null,
|
||||
label: segment.SegmentLabel,
|
||||
};
|
||||
});
|
||||
|
||||
// make a list of functional groups per segment
|
||||
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
|
||||
const segmentNumber =
|
||||
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
|
||||
segments[segmentNumber].functionalGroups.push(functionalGroup);
|
||||
});
|
||||
|
||||
return _getPixelData(dataset, segments);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dicom-seg',
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getSopClassHandlerModule;
|
||||
7
extensions/cornerstone-dicom-seg/src/id.js
Normal file
7
extensions/cornerstone-dicom-seg/src/id.js
Normal file
@ -0,0 +1,7 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dicom-seg';
|
||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||
|
||||
export { id, SOPClassHandlerId, SOPClassHandlerName };
|
||||
93
extensions/cornerstone-dicom-seg/src/index.tsx
Normal file
93
extensions/cornerstone-dicom-seg/src/index.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { id } from './id';
|
||||
import React from 'react';
|
||||
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'
|
||||
);
|
||||
});
|
||||
|
||||
const OHIFCornerstoneSEGViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
const extension = {
|
||||
/**
|
||||
* 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 = {},
|
||||
}) => {},
|
||||
/**
|
||||
* 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: ({ servicesManager, commandsManager, extensionManager }) => {
|
||||
const wrappedPanelSegmentation = () => {
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'panelSegmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentation,
|
||||
},
|
||||
];
|
||||
},
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneSEGViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport },
|
||||
];
|
||||
},
|
||||
/**
|
||||
* SopClassHandlerModule should provide a list of sop class handlers that will be
|
||||
* available in OHIF for Modes to consume and use to create displaySets from Series.
|
||||
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
|
||||
* Examples include the default sop class handler provided by the default extension
|
||||
*/
|
||||
getSopClassHandlerModule,
|
||||
};
|
||||
|
||||
export default extension;
|
||||
@ -0,0 +1,292 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationGroupTable } from '@ohif/ui';
|
||||
import callInputDialog from './callInputDialog';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import callColorPickerDialog from './callColorPickerDialog';
|
||||
|
||||
export default function PanelSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) {
|
||||
const {
|
||||
SegmentationService,
|
||||
UIDialogService,
|
||||
ViewportGridService,
|
||||
ToolGroupService,
|
||||
CornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [
|
||||
initialSegmentationConfigurations,
|
||||
setInitialSegmentationConfigurations,
|
||||
] = useState(SegmentationService.getConfiguration());
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() =>
|
||||
SegmentationService.getSegmentations()
|
||||
);
|
||||
|
||||
const [isMinimized, setIsMinimized] = useState({});
|
||||
|
||||
const onToggleMinimizeSegmentation = useCallback(
|
||||
id => {
|
||||
setIsMinimized(prevState => ({
|
||||
...prevState,
|
||||
[id]: !prevState[id],
|
||||
}));
|
||||
},
|
||||
[setIsMinimized]
|
||||
);
|
||||
|
||||
// Only expand the last segmentation added to the list and collapse the rest
|
||||
useEffect(() => {
|
||||
const lastSegmentationId = segmentations[segmentations.length - 1]?.id;
|
||||
if (lastSegmentationId) {
|
||||
setIsMinimized(prevState => ({
|
||||
...prevState,
|
||||
[lastSegmentationId]: false,
|
||||
}));
|
||||
}
|
||||
}, [segmentations, setIsMinimized]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const onSegmentationDelete = (segmentationId: string) => {
|
||||
SegmentationService.remove(segmentationId);
|
||||
};
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = SegmentationService.getToolGroupIdsWithSegmentation(
|
||||
segmentationId
|
||||
);
|
||||
|
||||
return toolGroupIds;
|
||||
};
|
||||
|
||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
||||
SegmentationService.setActiveSegmentForSegmentation(
|
||||
segmentationId,
|
||||
segmentIndex
|
||||
);
|
||||
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
// const toolGroupId =
|
||||
SegmentationService.setActiveSegmentationForToolGroup(
|
||||
segmentationId,
|
||||
toolGroupId
|
||||
);
|
||||
SegmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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.setSegmentLabelForSegmentation(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
label
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentationEdit = segmentationId => {
|
||||
const segmentation = SegmentationService.getSegmentation(segmentationId);
|
||||
const { label } = segmentation;
|
||||
|
||||
callInputDialog(UIDialogService, label, (label, actionId) => {
|
||||
if (label === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
SegmentationService.addOrUpdateSegmentation(
|
||||
{
|
||||
id: segmentationId,
|
||||
label,
|
||||
},
|
||||
false, // suppress event
|
||||
true // notYetUpdatedAtSource
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
||||
// Todo: Implement color picker later
|
||||
return;
|
||||
};
|
||||
|
||||
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
||||
// SegmentationService.removeSegmentFromSegmentation(
|
||||
// segmentationId,
|
||||
// segmentIndex
|
||||
// );
|
||||
console.warn('not implemented yet');
|
||||
};
|
||||
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = SegmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
const isVisible = !segmentInfo.isVisible;
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
// Todo: right now we apply the visibility to all tool groups
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
SegmentationService.setSegmentVisibility(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
isVisible,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleSegmentationVisibility = segmentationId => {
|
||||
SegmentationService.toggleSegmentationVisibility(segmentationId);
|
||||
};
|
||||
|
||||
const setSegmentationConfiguration = useCallback(
|
||||
(segmentationId, key, value) => {
|
||||
SegmentationService.setConfiguration({
|
||||
segmentationId,
|
||||
[key]: value,
|
||||
});
|
||||
},
|
||||
[SegmentationService]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between mt-1">
|
||||
{/* show segmentation table */}
|
||||
{segmentations?.length ? (
|
||||
<SegmentationGroupTable
|
||||
title={t('Segmentations')}
|
||||
showAddSegmentation={false}
|
||||
segmentations={segmentations}
|
||||
isMinimized={isMinimized}
|
||||
activeSegmentationId={selectedSegmentationId}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||
segmentationConfig={{
|
||||
initialConfig: initialSegmentationConfigurations,
|
||||
usePercentage: true,
|
||||
}}
|
||||
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
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PanelSegmentation.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,63 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog } 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: 'Enter Segment Label',
|
||||
value: { label },
|
||||
noCloseButton: true,
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Confirm', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
return (
|
||||
<div className="p-4 bg-primary-dark">
|
||||
<Input
|
||||
autoFocus
|
||||
className="mt-2 bg-black border-primary-main"
|
||||
type="text"
|
||||
containerClassName="mr-2"
|
||||
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' } });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default callInputDialog;
|
||||
@ -0,0 +1,35 @@
|
||||
import React, { useReducer } from 'react';
|
||||
|
||||
// Todo: use defaults in cs3d
|
||||
const initialState = {
|
||||
renderOutline: true,
|
||||
renderFill: true,
|
||||
outlineOpacity: 0.9,
|
||||
outlineWidth: 3,
|
||||
fillOpacity: 0.9,
|
||||
fillOpacityInactive: 0.8,
|
||||
renderInactiveSegmentations: true,
|
||||
};
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'RENDER_OUTLINE':
|
||||
return { ...state, renderOutline: action.payload.value };
|
||||
case 'RENDER_FILL':
|
||||
return { ...state, renderFill: action.payload.value };
|
||||
case 'SET_OUTLINE_OPACITY':
|
||||
return { ...state, outlineOpacity: action.payload.value };
|
||||
case 'SET_OUTLINE_WIDTH':
|
||||
return { ...state, outlineWidth: action.payload.value };
|
||||
case 'SET_FILL_OPACITY':
|
||||
return { ...state, fillOpacity: action.payload.value };
|
||||
case 'SET_FILL_OPACITY_INACTIVE':
|
||||
return { ...state, fillOpacityInactive: action.payload.value };
|
||||
case 'RENDER_INACTIVE_SEGMENTATIONS':
|
||||
return { ...state, renderInactiveSegmentations: action.payload.value };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { initialState, reducer };
|
||||
71
extensions/cornerstone-dicom-seg/src/utils/_hydrateSEG.ts
Normal file
71
extensions/cornerstone-dicom-seg/src/utils/_hydrateSEG.ts
Normal file
@ -0,0 +1,71 @@
|
||||
async function _hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
SegmentationService,
|
||||
HangingProtocolService,
|
||||
ViewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||
|
||||
let segmentationId = null;
|
||||
|
||||
// We need the hydration to notify panels about the new segmentation added
|
||||
const suppressEvents = false;
|
||||
|
||||
segmentationId = await SegmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
segmentationId,
|
||||
suppressEvents
|
||||
);
|
||||
|
||||
SegmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
|
||||
const { viewports } = ViewportGridService.getState();
|
||||
|
||||
const updatedViewports = HangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
ViewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
// Todo: fix this after we have a better way for stack viewport segmentations
|
||||
|
||||
// check every viewport in the viewports to see if the displaySetInstanceUID
|
||||
// is being displayed, if so we need to update the viewport to use volume viewport
|
||||
// (if already is not using it) since Cornerstone3D currently only supports
|
||||
// volume viewport for segmentation
|
||||
viewports.forEach((viewport, index) => {
|
||||
if (index === viewportIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldDisplaySeg = SegmentationService.shouldRenderSegmentation(
|
||||
viewport.displaySetInstanceUIDs,
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
ViewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: index,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
toolGroupId,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default _hydrateSEGDisplaySet;
|
||||
@ -0,0 +1,34 @@
|
||||
function createSEGToolGroupAndAddTools(
|
||||
ToolGroupService,
|
||||
toolGroupId,
|
||||
extensionManager
|
||||
) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
);
|
||||
|
||||
const { toolNames, Enums } = utilityModule.exports;
|
||||
|
||||
const tools = {
|
||||
active: [
|
||||
{
|
||||
toolName: toolNames.WindowLevel,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Pan,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Zoom,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||
},
|
||||
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
|
||||
],
|
||||
enabled: [{ toolName: toolNames.SegmentationDisplay }],
|
||||
};
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
||||
}
|
||||
|
||||
export default createSEGToolGroupAndAddTools;
|
||||
@ -0,0 +1,70 @@
|
||||
import hydrateSEGDisplaySet from './_hydrateSEG';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
HYDRATE_SEG: 5,
|
||||
};
|
||||
|
||||
function promptHydrateSEG({
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
toolGroupId = 'default',
|
||||
}) {
|
||||
const { UIViewportDialogService } = servicesManager.services;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
const promptResult = await _askHydrate(
|
||||
UIViewportDialogService,
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(UIViewportDialogService, viewportIndex) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
type: 'secondary',
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
UIViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
UIViewportDialogService.show({
|
||||
viewportIndex,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
UIViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default promptHydrateSEG;
|
||||
@ -0,0 +1,424 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
LoadingIndicatorProgress,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
const { formatDate } = utils;
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
|
||||
function OHIFCornerstoneSEGViewport(props) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SEGViewport');
|
||||
|
||||
const {
|
||||
DisplaySetService,
|
||||
ToolGroupService,
|
||||
SegmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
||||
|
||||
// SEG viewport will always have a single display set
|
||||
if (displaySets.length > 1) {
|
||||
throw new Error('SEG viewport should only have a single display set');
|
||||
}
|
||||
|
||||
const segDisplaySet = displaySets[0];
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||
|
||||
// States
|
||||
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||
|
||||
// Hydration means that the SEG is opened and segments are loaded into the
|
||||
// segmentation panel, and SEG is also rendered on any viewport that is in the
|
||||
// same frameOfReferenceUID as the referencedSeriesUID of the SEG. However,
|
||||
// loading basically means SEG loading over network and bit unpacking of the
|
||||
// SEG data.
|
||||
const [isHydrated, setIsHydrated] = useState(segDisplaySet.isHydrated);
|
||||
const [segIsLoading, setSegIsLoading] = useState(!segDisplaySet.isLoaded);
|
||||
const [element, setElement] = useState(null);
|
||||
const [processingProgress, setProcessingProgress] = useState({
|
||||
segmentIndex: 1,
|
||||
totalSegments: null,
|
||||
});
|
||||
|
||||
// refs
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
|
||||
const { viewports, activeViewportIndex } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet
|
||||
);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
displaySet: referencedDisplaySet,
|
||||
metadata: referencedDisplaySetMetadata,
|
||||
};
|
||||
/**
|
||||
* OnElementEnabled callback which is called after the cornerstoneExtension
|
||||
* has enabled the element. Note: we delegate all the image rendering to
|
||||
* cornerstoneExtension, so we don't need to do anything here regarding
|
||||
* the image rendering, element enabling etc.
|
||||
*/
|
||||
const onElementEnabled = evt => {
|
||||
setElement(evt.detail.element);
|
||||
};
|
||||
|
||||
const onElementDisabled = () => {
|
||||
setElement(null);
|
||||
};
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const {
|
||||
displaySet: referencedDisplaySet,
|
||||
} = referencedDisplaySetRef.current;
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, segDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementDisabled={onElementDisabled}
|
||||
// initialImageIndex={initialImageIndex}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportIndex, segDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = SegmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const { segments } = segmentation;
|
||||
|
||||
const numberOfSegments = Object.keys(segments).length;
|
||||
|
||||
let newSelectedSegmentIndex = selectedSegment + direction;
|
||||
|
||||
if (newSelectedSegmentIndex > numberOfSegments - 1) {
|
||||
newSelectedSegmentIndex = 1;
|
||||
} else if (newSelectedSegmentIndex === 0) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
SegmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
newSelectedSegmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (segIsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
segDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
}, [servicesManager, viewportIndex, segDisplaySet, segIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = SegmentationService.subscribe(
|
||||
SegmentationService.EVENTS.SEGMENTATION_PIXEL_DATA_CREATED,
|
||||
evt => {
|
||||
if (
|
||||
evt.segDisplaySet.displaySetInstanceUID ===
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
) {
|
||||
setSegIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = SegmentationService.subscribe(
|
||||
SegmentationService.EVENTS.SEGMENT_PIXEL_DATA_CREATED,
|
||||
({ segmentIndex, numSegments }) => {
|
||||
setProcessingProgress({
|
||||
segmentIndex,
|
||||
totalSegments: numSegments,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
|
||||
/**
|
||||
Cleanup the SEG viewport when the viewport is destroyed
|
||||
*/
|
||||
useEffect(() => {
|
||||
const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe(
|
||||
DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
if (
|
||||
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
|
||||
) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
onDisplaySetsRemovedSubscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let toolGroup = ToolGroupService.getToolGroup(toolGroupId);
|
||||
|
||||
if (toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroup = createSEGToolGroupAndAddTools(
|
||||
ToolGroupService,
|
||||
toolGroupId,
|
||||
extensionManager
|
||||
);
|
||||
|
||||
setToolGroupCreated(true);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
SegmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
ToolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsHydrated(segDisplaySet.isHydrated);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
SegmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (
|
||||
!referencedDisplaySetRef.current ||
|
||||
referencedDisplaySet.displaySetInstanceUID !==
|
||||
referencedDisplaySetRef.current.displaySet.displaySetInstanceUID
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (children && children.length) {
|
||||
childrenWithProps = children.map((child, index) => {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onPillClick = () => {
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
segDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onSegmentChange}
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onPillClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: `SEG Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||
: '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
{segIsLoading && (
|
||||
<LoadingIndicatorProgress
|
||||
className="w-full h-full"
|
||||
progress={
|
||||
processingProgress.totalSegments !== null
|
||||
? ((processingProgress.segmentIndex + 1) /
|
||||
processingProgress.totalSegments) *
|
||||
100
|
||||
: null
|
||||
}
|
||||
textBlock={
|
||||
!processingProgress.totalSegments ? (
|
||||
<span className="text-white text-sm">Loading SEG ...</span>
|
||||
) : (
|
||||
<span className="text-white text-sm flex items-baseline space-x-2">
|
||||
<div>Loading Segment</div>
|
||||
<div className="w-3">{`${processingProgress.segmentIndex}`}</div>
|
||||
<div>/</div>
|
||||
<div>{`${processingProgress.totalSegments}`}</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{getCornerstoneViewport()}
|
||||
<div className="absolute w-full">
|
||||
{viewportDialogState.viewportIndex === viewportIndex && (
|
||||
<Notification
|
||||
id="viewport-notification"
|
||||
message={viewportDialogState.message}
|
||||
type={viewportDialogState.type}
|
||||
actions={viewportDialogState.actions}
|
||||
onSubmit={viewportDialogState.onSubmit}
|
||||
onOutsideClick={viewportDialogState.onOutsideClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OHIFCornerstoneSEGViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
};
|
||||
|
||||
OHIFCornerstoneSEGViewport.defaultProps = {
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
PatientID: image0.PatientID,
|
||||
PatientName: image0.PatientName,
|
||||
PatientSex: image0.PatientSex,
|
||||
PatientAge: image0.PatientAge,
|
||||
SliceThickness: image0.SliceThickness,
|
||||
StudyDate: image0.StudyDate,
|
||||
SeriesDescription: image0.SeriesDescription,
|
||||
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||
SeriesNumber: image0.SeriesNumber,
|
||||
ManufacturerModelName: image0.ManufacturerModelName,
|
||||
SpacingBetweenSlices: image0.SpacingBetweenSlices,
|
||||
};
|
||||
|
||||
return referencedDisplaySetMetadata;
|
||||
}
|
||||
|
||||
export default OHIFCornerstoneSEGViewport;
|
||||
@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
import _hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
|
||||
export default function _getStatusComponent({ isHydrated, onPillClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
backgroundColor: '#98e5c1',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="exclamation"
|
||||
style={{ color: '#000', width: '12px', height: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>This Segmentation is loaded in the segmentation panel</div>
|
||||
);
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
style={{ color: '#000', width: '14px', height: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>Click to load segmentation.</div>;
|
||||
}
|
||||
|
||||
const StatusPill = () => (
|
||||
<div
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-center px-8 rounded-full cursor-default bg-customgreen-100',
|
||||
{
|
||||
'hover:bg-customblue-100': !isHydrated,
|
||||
'cursor-pointer': !isHydrated,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
height: '24px',
|
||||
width: '55px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!isHydrated) {
|
||||
if (onPillClick) {
|
||||
onPillClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="pr-1 text-base font-medium leading-none text-black">
|
||||
SEG
|
||||
</div>
|
||||
<StatusIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<StatusPill />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusPill />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -33,19 +33,19 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"@ohif/extension-cornerstone": "^3.0.0",
|
||||
"@ohif/extension-measurement-tracking": "^3.0.0",
|
||||
"@ohif/ui": "^2.0.0",
|
||||
"dcmjs": "^0.28.3",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"@ohif/extension-cornerstone": "^3.0.0",
|
||||
"@ohif/extension-measurement-tracking": "^3.0.0"
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.3",
|
||||
"classnames": "^2.2.6",
|
||||
"@cornerstonejs/core": "^0.16.8",
|
||||
"@cornerstonejs/tools": "^0.24.1"
|
||||
"@cornerstonejs/core": "^0.21.0",
|
||||
"@cornerstonejs/tools": "^0.29.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesNumber,
|
||||
SeriesDate,
|
||||
ConceptNameCodeSequence,
|
||||
SOPClassUID,
|
||||
} = instance;
|
||||
|
||||
if (
|
||||
@ -103,6 +104,7 @@ function _getDisplaySetsFromSeries(
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
isDerivedDisplaySet: true,
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import {
|
||||
Notification,
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
Tooltip,
|
||||
Icon,
|
||||
} from '@ohif/ui';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const { formatDate } = utils;
|
||||
|
||||
@ -27,6 +32,8 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
extensionManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SRViewport');
|
||||
|
||||
const {
|
||||
DisplaySetService,
|
||||
CornerstoneViewportService,
|
||||
@ -318,8 +325,6 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
});
|
||||
}
|
||||
|
||||
const { Modality } = srDisplaySet;
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
@ -341,24 +346,23 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onPillClick={() => {
|
||||
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
onArrowsClick={onMeasurementChange}
|
||||
getStatusComponent={() =>
|
||||
_getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportIndex,
|
||||
});
|
||||
}}
|
||||
onSeriesChange={onMeasurementChange}
|
||||
isTracked: false,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
})
|
||||
}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
isTracked: false,
|
||||
isLocked,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isHydrated,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription,
|
||||
modality: Modality,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
@ -439,6 +443,139 @@ async function _getViewportReferencedDisplaySetData(
|
||||
return { referencedDisplaySetMetadata, referencedDisplaySet };
|
||||
}
|
||||
|
||||
function _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportIndex,
|
||||
isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
}) {
|
||||
const onPillClick = () => {
|
||||
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
viewportIndex,
|
||||
});
|
||||
};
|
||||
|
||||
// 1 - Incompatible
|
||||
// 2 - Locked
|
||||
// 3 - Rehydratable / Open
|
||||
const state =
|
||||
isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
switch (state) {
|
||||
case 1:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
backgroundColor: '#98e5c1',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="exclamation"
|
||||
style={{ color: '#000', width: '12px', height: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
This structured report is not compatible
|
||||
<br />
|
||||
with this application.
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-black rounded-full"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="lock"
|
||||
style={{ color: '#05D97C', width: '8px', height: '11px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>
|
||||
This structured report is currently read-only
|
||||
<br />
|
||||
because you are tracking measurements in
|
||||
<br />
|
||||
another viewport.
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
StatusIcon = () => (
|
||||
<div
|
||||
className="flex items-center justify-center -mr-1 bg-white rounded-full group-hover:bg-customblue-200"
|
||||
style={{
|
||||
width: '18px',
|
||||
height: '18px',
|
||||
border: 'solid 1.5px #000000',
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
style={{ color: '#000', width: '14px', height: '14px' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>Click to restore measurements.</div>;
|
||||
}
|
||||
|
||||
const StatusPill = () => (
|
||||
<div
|
||||
className={classNames(
|
||||
'group relative flex items-center justify-center px-2 rounded-full cursor-default bg-customgreen-100',
|
||||
{
|
||||
'hover:bg-customblue-100': state === 3,
|
||||
'cursor-pointer': state === 3,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
height: '24px',
|
||||
width: '55px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (state === 3) {
|
||||
if (onPillClick) {
|
||||
onPillClick();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="pr-1 text-lg font-bold leading-none text-black">SR</span>
|
||||
<StatusIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<StatusPill />
|
||||
</Tooltip>
|
||||
)}
|
||||
{!ToolTipMessage && <StatusPill />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// function _onDoubleClick() {
|
||||
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||
// 'manipulatorState'
|
||||
|
||||
@ -43,11 +43,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.17.9",
|
||||
"@cornerstonejs/core": "^0.16.8",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.4.23",
|
||||
"@cornerstonejs/tools": "^0.24.1",
|
||||
"@kitware/vtk.js": "^24.18.7",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"@cornerstonejs/core": "^0.21.0",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^0.6.1",
|
||||
"@cornerstonejs/tools": "^0.29.2",
|
||||
"@kitware/vtk.js": "25.9.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"shader-loader": "^1.3.1",
|
||||
|
||||
@ -8,10 +8,11 @@ import {
|
||||
eventTarget,
|
||||
getEnabledElement,
|
||||
StackViewport,
|
||||
utilities as csUtils,
|
||||
CONSTANTS,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
|
||||
|
||||
import './OHIFCornerstoneViewport.css';
|
||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||
@ -46,24 +47,44 @@ function areEqual(prevProps, nextProps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const prevDisplaySets = prevProps.displaySets[0];
|
||||
const nextDisplaySets = nextProps.displaySets[0];
|
||||
const prevDisplaySets = prevProps.displaySets;
|
||||
const nextDisplaySets = nextProps.displaySets;
|
||||
|
||||
if (prevDisplaySets && nextDisplaySets) {
|
||||
const areSameDisplaySetInstanceUIDs =
|
||||
prevDisplaySets.displaySetInstanceUID ===
|
||||
nextDisplaySets.displaySetInstanceUID;
|
||||
const areSameImageLength =
|
||||
prevDisplaySets.images.length === nextDisplaySets.images.length;
|
||||
const areSameImageIds = prevDisplaySets.images.every(
|
||||
(prevImage, index) =>
|
||||
prevImage.imageId === nextDisplaySets.images[index].imageId
|
||||
);
|
||||
return (
|
||||
areSameDisplaySetInstanceUIDs && areSameImageLength && areSameImageIds
|
||||
);
|
||||
if (prevDisplaySets.length !== nextDisplaySets.length) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
for (let i = 0; i < prevDisplaySets.length; i++) {
|
||||
const prevDisplaySet = prevDisplaySets[i];
|
||||
|
||||
const foundDisplaySet = nextDisplaySets.find(
|
||||
nextDisplaySet =>
|
||||
nextDisplaySet.displaySetInstanceUID ===
|
||||
prevDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!foundDisplaySet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check they contain the same image
|
||||
if (foundDisplaySet.images?.length !== prevDisplaySet.images?.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if their imageIds are the same
|
||||
if (foundDisplaySet.images?.length) {
|
||||
for (let j = 0; j < foundDisplaySet.images.length; j++) {
|
||||
if (
|
||||
foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||
@ -85,8 +106,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
} = props;
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [viewportData, setViewportData] = useState(null);
|
||||
const [_, viewportGridService] = useViewportGrid();
|
||||
|
||||
const elementRef = useRef();
|
||||
|
||||
@ -97,6 +116,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
ToolGroupService,
|
||||
SyncGroupService,
|
||||
CornerstoneViewportService,
|
||||
CornerstoneCacheService,
|
||||
ViewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// useCallback for scroll bar height calculation
|
||||
@ -153,7 +174,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
// disable the element upon unmounting
|
||||
useEffect(() => {
|
||||
CornerstoneViewportService.enableElement(
|
||||
CornerstoneViewportService.enableViewport(
|
||||
viewportIndex,
|
||||
viewportOptions,
|
||||
elementRef.current
|
||||
@ -175,7 +196,11 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
|
||||
ToolGroupService.disable(viewportId, renderingEngineId);
|
||||
ToolGroupService.removeViewportFromToolGroup(
|
||||
viewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
SyncGroupService.removeViewportFromSyncGroup(
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
@ -184,14 +209,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
|
||||
CornerstoneViewportService.disableElement(viewportIndex);
|
||||
|
||||
if (onElementDisabled) {
|
||||
onElementDisabled(viewportInfo);
|
||||
}
|
||||
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.ELEMENT_ENABLED,
|
||||
elementEnabledHandler
|
||||
);
|
||||
|
||||
if (onElementDisabled) {
|
||||
onElementDisabled();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -207,11 +232,12 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const { unsubscribe } = DisplaySetService.subscribe(
|
||||
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||
async invalidatedDisplaySetInstanceUID => {
|
||||
if (
|
||||
viewportData.displaySetInstanceUIDs.includes(
|
||||
invalidatedDisplaySetInstanceUID
|
||||
)
|
||||
) {
|
||||
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
||||
const viewportData = viewportInfo.getViewportData();
|
||||
const newViewportData = await CornerstoneCacheService.invalidateViewportData(
|
||||
viewportData,
|
||||
invalidatedDisplaySetInstanceUID,
|
||||
@ -225,15 +251,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
newViewportData,
|
||||
keepCamera
|
||||
);
|
||||
|
||||
setViewportData(newViewportData);
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportData, viewportIndex]);
|
||||
}, [viewportIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
// handle the default viewportType to be stack
|
||||
@ -242,22 +266,19 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
}
|
||||
|
||||
const loadViewportData = async () => {
|
||||
await CornerstoneCacheService.getViewportData(
|
||||
viewportIndex,
|
||||
const viewportData = await CornerstoneCacheService.createViewportData(
|
||||
displaySets,
|
||||
viewportOptions.viewportType,
|
||||
viewportOptions,
|
||||
dataSource,
|
||||
viewportDataLoaded => {
|
||||
CornerstoneViewportService.setViewportDisplaySets(
|
||||
viewportIndex,
|
||||
viewportDataLoaded,
|
||||
viewportOptions,
|
||||
displaySetOptions
|
||||
);
|
||||
setViewportData(viewportDataLoaded);
|
||||
},
|
||||
initialImageIndex
|
||||
);
|
||||
|
||||
CornerstoneViewportService.setViewportData(
|
||||
viewportIndex,
|
||||
viewportData,
|
||||
viewportOptions,
|
||||
displaySetOptions
|
||||
);
|
||||
};
|
||||
|
||||
loadViewportData();
|
||||
@ -280,7 +301,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
displaySets,
|
||||
viewportGridService
|
||||
ViewportGridService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
|
||||
_checkForCachedJumpToMeasurementEvents(
|
||||
@ -289,13 +311,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
displaySets,
|
||||
viewportGridService
|
||||
ViewportGridService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
};
|
||||
}, [displaySets, elementRef, viewportIndex, viewportData]);
|
||||
}, [displaySets, elementRef, viewportIndex]);
|
||||
|
||||
return (
|
||||
<div className="viewport-wrapper">
|
||||
@ -303,7 +326,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
handleWidth
|
||||
handleHeight
|
||||
skipOnMount={true} // Todo: make these configurable
|
||||
refreshMode={'debounce'}
|
||||
refreshMode={'throttle'}
|
||||
refreshRate={100}
|
||||
onResize={onResize}
|
||||
targetRef={elementRef.current}
|
||||
@ -332,7 +355,8 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
displaySets,
|
||||
viewportGridService
|
||||
viewportGridService,
|
||||
CornerstoneViewportService
|
||||
) {
|
||||
const displaysUIDs = displaySets.map(
|
||||
displaySet => displaySet.displaySetInstanceUID
|
||||
@ -350,7 +374,8 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
viewportIndex,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
viewportGridService
|
||||
ViewportGridService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -366,7 +391,8 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
displaySets,
|
||||
viewportGridService
|
||||
ViewportGridService,
|
||||
CornerstoneViewportService
|
||||
) {
|
||||
const displaysUIDs = displaySets.map(
|
||||
displaySet => displaySet.displaySetInstanceUID
|
||||
@ -389,7 +415,8 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
viewportIndex,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
viewportGridService
|
||||
ViewportGridService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -401,7 +428,8 @@ function _jumpToMeasurement(
|
||||
viewportIndex,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
viewportGridService
|
||||
ViewportGridService,
|
||||
CornerstoneViewportService
|
||||
) {
|
||||
const targetElement = targetElementRef.current;
|
||||
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
|
||||
@ -418,14 +446,20 @@ function _jumpToMeasurement(
|
||||
// to set it properly
|
||||
// setCornerstoneMeasurementActive(measurement);
|
||||
|
||||
viewportGridService.setActiveViewportIndex(viewportIndex);
|
||||
ViewportGridService.setActiveViewportIndex(viewportIndex);
|
||||
|
||||
const enableElement = getEnabledElement(targetElement);
|
||||
|
||||
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (enableElement) {
|
||||
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
||||
const viewport = enableElement.viewport as IStackViewport | IVolumeViewport;
|
||||
|
||||
let imageIdIndex = 0;
|
||||
let viewportCameraDirectionMatch = true;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const imageIds = viewport.getImageIds();
|
||||
@ -440,9 +474,29 @@ function _jumpToMeasurement(
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// for volume viewport we can't rely on the imageIdIndex since it can be
|
||||
// a reconstructed view that doesn't match the original slice numbers etc.
|
||||
const { viewPlaneNormal } = measurement.metadata;
|
||||
imageIdIndex = referencedDisplaySet.images.findIndex(
|
||||
i => i.SOPInstanceUID === SOPInstanceUID
|
||||
);
|
||||
|
||||
const { orientation } = viewportInfo.getViewportOptions();
|
||||
|
||||
if (
|
||||
orientation &&
|
||||
viewPlaneNormal &&
|
||||
!csUtils.isEqual(
|
||||
CONSTANTS.MPR_CAMERA_VALUES[orientation]?.viewPlaneNormal,
|
||||
viewPlaneNormal
|
||||
)
|
||||
) {
|
||||
viewportCameraDirectionMatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewportCameraDirectionMatch || imageIdIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
cs3DTools.utilities.jumpToSlice(targetElement, {
|
||||
|
||||
@ -3,8 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import ViewportImageScrollbar from './ViewportImageScrollbar';
|
||||
import ViewportOverlay from './ViewportOverlay';
|
||||
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
||||
import CornerstoneCacheService from '../../services/ViewportService/CornerstoneCacheService';
|
||||
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
||||
|
||||
function CornerstoneOverlays(props) {
|
||||
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
|
||||
@ -16,8 +15,8 @@ function CornerstoneOverlays(props) {
|
||||
const [viewportData, setViewportData] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = CornerstoneCacheService.subscribe(
|
||||
CornerstoneCacheService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||
const { unsubscribe } = CornerstoneViewportService.subscribe(
|
||||
CornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||
props => {
|
||||
if (props.viewportIndex !== viewportIndex) {
|
||||
return;
|
||||
@ -47,7 +46,7 @@ function CornerstoneOverlays(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="noselect">
|
||||
<ViewportImageScrollbar
|
||||
viewportIndex={viewportIndex}
|
||||
viewportData={viewportData}
|
||||
@ -64,11 +63,15 @@ function CornerstoneOverlays(props) {
|
||||
servicesManager={servicesManager}
|
||||
element={element}
|
||||
/>
|
||||
<ViewportLoadingIndicator viewportData={viewportData} element={element} />
|
||||
<ViewportImageSliceLoadingIndicator
|
||||
viewportData={viewportData}
|
||||
element={element}
|
||||
/>
|
||||
<ViewportOrientationMarkers
|
||||
imageSliceData={imageSliceData}
|
||||
element={element}
|
||||
viewportData={viewportData}
|
||||
servicesManager={servicesManager}
|
||||
viewportIndex={viewportIndex}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -57,7 +57,7 @@ function CornerstoneImageScrollbar({
|
||||
|
||||
setImageSliceData({
|
||||
imageIndex: imageIndex,
|
||||
numberOfSlices: viewportData.imageIds.length,
|
||||
numberOfSlices: viewportData.data.imageIds.length,
|
||||
});
|
||||
|
||||
return;
|
||||
@ -87,7 +87,7 @@ function CornerstoneImageScrollbar({
|
||||
// find the index of imageId in the imageIds
|
||||
setImageSliceData({
|
||||
imageIndex: newImageIdIndex,
|
||||
numberOfSlices: viewportData.imageIds.length,
|
||||
numberOfSlices: viewportData.data.imageIds.length,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
function ViewportLoadingIndicator({ viewportData, element }) {
|
||||
function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
@ -89,15 +89,15 @@ function ViewportLoadingIndicator({ viewportData, element }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ViewportLoadingIndicator.propTypes = {
|
||||
ViewportImageSliceLoadingIndicator.propTypes = {
|
||||
percentComplete: PropTypes.number,
|
||||
error: PropTypes.object,
|
||||
element: PropTypes.object,
|
||||
};
|
||||
|
||||
ViewportLoadingIndicator.defaultProps = {
|
||||
ViewportImageSliceLoadingIndicator.defaultProps = {
|
||||
percentComplete: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export default ViewportLoadingIndicator;
|
||||
export default ViewportImageSliceLoadingIndicator;
|
||||
@ -11,7 +11,7 @@
|
||||
position: absolute;
|
||||
}
|
||||
.ViewportOrientationMarkers .top-mid {
|
||||
top: 5px;
|
||||
top: 0.6rem;
|
||||
left: 50%;
|
||||
}
|
||||
.ViewportOrientationMarkers .left-mid {
|
||||
@ -23,7 +23,7 @@
|
||||
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
||||
}
|
||||
.ViewportOrientationMarkers .bottom-mid {
|
||||
top: calc(100% - var(--marker-height) - 5px);
|
||||
top: calc(100% - var(--marker-height) - 0.6rem);
|
||||
left: 47%;
|
||||
}
|
||||
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
||||
|
||||
@ -1,10 +1,158 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, Types } from '@cornerstonejs/core';
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import {
|
||||
metaData,
|
||||
Enums,
|
||||
Types,
|
||||
getEnabledElement,
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
import PropTypes from 'prop-types';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
import './ViewportOrientationMarkers.css';
|
||||
import { getEnabledElement } from '../../state';
|
||||
|
||||
const {
|
||||
getOrientationStringLPS,
|
||||
invertOrientationStringLPS,
|
||||
} = utilities.orientation;
|
||||
|
||||
function ViewportOrientationMarkers({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
orientationMarkers = ['top', 'left'],
|
||||
}) {
|
||||
// Rotation is in degrees
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [flipHorizontal, setFlipHorizontal] = useState(false);
|
||||
const [flipVertical, setFlipVertical] = useState(false);
|
||||
const { CornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
useEffect(() => {
|
||||
const cameraModifiedListener = (
|
||||
evt: Types.EventTypes.CameraModifiedEvent
|
||||
) => {
|
||||
const { rotation, previousCamera, camera } = evt.detail;
|
||||
|
||||
if (rotation !== undefined) {
|
||||
setRotation(rotation);
|
||||
}
|
||||
|
||||
if (
|
||||
camera.flipHorizontal !== undefined &&
|
||||
previousCamera.flipHorizontal !== camera.flipHorizontal
|
||||
) {
|
||||
setFlipHorizontal(camera.flipHorizontal);
|
||||
}
|
||||
|
||||
if (
|
||||
camera.flipVertical !== undefined &&
|
||||
previousCamera.flipVertical !== camera.flipVertical
|
||||
) {
|
||||
setFlipVertical(camera.flipVertical);
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener(
|
||||
Enums.Events.CAMERA_MODIFIED,
|
||||
cameraModifiedListener
|
||||
);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(
|
||||
Enums.Events.CAMERA_MODIFIED,
|
||||
cameraModifiedListener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const markers = useMemo(() => {
|
||||
if (!viewportData) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let rowCosines, columnCosines;
|
||||
if (viewportData.viewportType === 'stack') {
|
||||
const imageIndex = imageSliceData.imageIndex;
|
||||
const imageId = viewportData.data.imageIds?.[imageIndex];
|
||||
|
||||
// Workaround for below TODO stub
|
||||
if (!imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
({ rowCosines, columnCosines } =
|
||||
metaData.get('imagePlaneModule', imageId) || {});
|
||||
} else {
|
||||
if (!element || !getEnabledElement(element)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { viewport } = getEnabledElement(element);
|
||||
const { viewUp, viewPlaneNormal } = viewport.getCamera();
|
||||
|
||||
const viewRight = vec3.create();
|
||||
vec3.cross(viewRight, viewUp, viewPlaneNormal);
|
||||
|
||||
columnCosines = [-viewUp[0], -viewUp[1], -viewUp[2]];
|
||||
rowCosines = viewRight;
|
||||
}
|
||||
|
||||
if (!rowCosines || !columnCosines || rotation === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const markers = _getOrientationMarkers(
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
rotation,
|
||||
flipVertical,
|
||||
flipHorizontal
|
||||
);
|
||||
|
||||
const ohifViewport = CornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
const backgroundColor = ohifViewport.getViewportOptions().background;
|
||||
|
||||
const isLight = backgroundColor
|
||||
? csUtils.isEqual(backgroundColor, [1, 1, 1])
|
||||
: false;
|
||||
|
||||
return orientationMarkers.map((m, index) => (
|
||||
<div
|
||||
className={`${m}-mid orientation-marker`}
|
||||
key={`${m}-mid orientation-marker`}
|
||||
>
|
||||
<div className="orientation-marker-value">{markers[m]}</div>
|
||||
</div>
|
||||
));
|
||||
}, [
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
rotation,
|
||||
flipVertical,
|
||||
flipHorizontal,
|
||||
orientationMarkers,
|
||||
element,
|
||||
]);
|
||||
|
||||
return <div className="ViewportOrientationMarkers noselect">{markers}</div>;
|
||||
}
|
||||
|
||||
ViewportOrientationMarkers.propTypes = {
|
||||
percentComplete: PropTypes.number,
|
||||
error: PropTypes.object,
|
||||
};
|
||||
|
||||
ViewportOrientationMarkers.defaultProps = {
|
||||
percentComplete: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
@ -16,17 +164,13 @@ import { getEnabledElement } from '../../state';
|
||||
* @param {*} rotation in degrees
|
||||
* @returns
|
||||
*/
|
||||
function getOrientationMarkers(
|
||||
function _getOrientationMarkers(
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
rotation,
|
||||
flipVertical,
|
||||
flipHorizontal
|
||||
) {
|
||||
const {
|
||||
getOrientationStringLPS,
|
||||
invertOrientationStringLPS,
|
||||
} = utilities.orientation;
|
||||
const rowString = getOrientationStringLPS(rowCosines);
|
||||
const columnString = getOrientationStringLPS(columnCosines);
|
||||
const oppositeRowString = invertOrientationStringLPS(rowString);
|
||||
@ -79,117 +223,4 @@ function getOrientationMarkers(
|
||||
return markers;
|
||||
}
|
||||
|
||||
function ViewportOrientationMarkers({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
orientationMarkers = ['top', 'left'],
|
||||
}) {
|
||||
// Rotation is in degrees
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [flipHorizontal, setFlipHorizontal] = useState(false);
|
||||
const [flipVertical, setFlipVertical] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const cameraModifiedListener = (
|
||||
evt: Types.EventTypes.CameraModifiedEvent
|
||||
) => {
|
||||
|
||||
const { rotation, previousCamera, camera } = evt.detail;
|
||||
|
||||
if (rotation !== undefined) {
|
||||
setRotation(rotation);
|
||||
}
|
||||
|
||||
if (camera.flipHorizontal !== undefined &&
|
||||
previousCamera.flipHorizontal !== camera.flipHorizontal) {
|
||||
setFlipHorizontal(camera.flipHorizontal);
|
||||
}
|
||||
|
||||
if (camera.flipVertical !== undefined &&
|
||||
previousCamera.flipVertical !== camera.flipVertical) {
|
||||
setFlipVertical(camera.flipVertical);
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener(
|
||||
Enums.Events.CAMERA_MODIFIED,
|
||||
cameraModifiedListener
|
||||
);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(
|
||||
Enums.Events.CAMERA_MODIFIED,
|
||||
cameraModifiedListener
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getMarkers = useCallback(
|
||||
orientationMarkers => {
|
||||
// Todo: support orientation markers for the volume viewports
|
||||
if (
|
||||
!viewportData ||
|
||||
viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const imageIndex = imageSliceData.imageIndex;
|
||||
const imageId = viewportData.imageIds?.[imageIndex];
|
||||
|
||||
// Workaround for below TODO stub
|
||||
if (!imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { rowCosines, columnCosines } =
|
||||
metaData.get('imagePlaneModule', imageId) || {};
|
||||
|
||||
if (!rowCosines || !columnCosines || rotation === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!rowCosines || !columnCosines) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const markers = getOrientationMarkers(
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
rotation,
|
||||
flipVertical,
|
||||
flipHorizontal
|
||||
);
|
||||
|
||||
return orientationMarkers.map((m, index) => (
|
||||
<div
|
||||
className={`${m}-mid orientation-marker`}
|
||||
key={`${m}-mid orientation-marker`}
|
||||
>
|
||||
<div className="orientation-marker-value">{markers[m]}</div>
|
||||
</div>
|
||||
));
|
||||
},
|
||||
[flipHorizontal, flipVertical, rotation, viewportData, imageSliceData]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ViewportOrientationMarkers noselect">
|
||||
{getMarkers(orientationMarkers)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ViewportOrientationMarkers.propTypes = {
|
||||
percentComplete: PropTypes.number,
|
||||
error: PropTypes.object,
|
||||
};
|
||||
|
||||
ViewportOrientationMarkers.defaultProps = {
|
||||
percentComplete: 0,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export default ViewportOrientationMarkers;
|
||||
|
||||
@ -28,6 +28,25 @@ function CornerstoneViewportOverlay({
|
||||
setActiveTools(ToolBarService.getActiveTools());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
const { unsubscribe } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
if (!isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveTools(ToolBarService.getActiveTools());
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updating the VOI when the viewport changes its voi
|
||||
*/
|
||||
@ -100,23 +119,6 @@ function CornerstoneViewportOverlay({
|
||||
};
|
||||
}, [viewportIndex, viewportData]);
|
||||
|
||||
/**
|
||||
* Updating the active tools when the toolbar changes
|
||||
*/
|
||||
// Todo: this should act on the toolGroups instead of the toolbar state
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => {
|
||||
setActiveTools(ToolBarService.getActiveTools());
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [ToolBarService]);
|
||||
|
||||
const getTopLeftContent = useCallback(() => {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
|
||||
@ -186,12 +188,6 @@ function CornerstoneViewportOverlay({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewportData.imageIds.length === 0) {
|
||||
throw new Error(
|
||||
'ViewportOverlay: only viewports with imageIds is supported at this time'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
topLeft={getTopLeftContent()}
|
||||
@ -201,7 +197,7 @@ function CornerstoneViewportOverlay({
|
||||
}
|
||||
|
||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
const imageIds = viewportData.imageIds;
|
||||
const imageIds = viewportData.data.imageIds;
|
||||
const imageId = imageIds[imageIndex];
|
||||
|
||||
if (!imageId) {
|
||||
|
||||
@ -1,34 +1,44 @@
|
||||
import {
|
||||
getEnabledElement,
|
||||
StackViewport,
|
||||
volumeLoader,
|
||||
cache,
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import {
|
||||
ToolGroupManager,
|
||||
Enums,
|
||||
segmentation,
|
||||
utilities as csToolsUtils,
|
||||
utilities as cstUtils,
|
||||
segmentation as cstSegmentation,
|
||||
ReferenceLinesTool,
|
||||
synchronizers as cstSynchronizers,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import { Types } from '@ohif/core';
|
||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
||||
import callInputDialog from './utils/callInputDialog';
|
||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
import getProtocolViewportStructureFromGridViewports from './utils/getProtocolViewportStructureFromGridViewports';
|
||||
import removeToolGroupSegmentationRepresentations from './utils/removeToolGroupSegmentationRepresentations';
|
||||
import calculateViewportRegistrations from './utils/calculateViewportRegistrations';
|
||||
|
||||
const MPR_TOOLGROUP_ID = 'mpr';
|
||||
|
||||
// [ {
|
||||
// synchronizerId: string,
|
||||
// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...]
|
||||
// ]}
|
||||
let STACK_IMAGE_SYNC_GROUPS_INFO = [];
|
||||
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
const {
|
||||
ViewportGridService,
|
||||
ToolGroupService,
|
||||
DisplaySetService,
|
||||
SyncGroupService,
|
||||
CineService,
|
||||
ToolBarService,
|
||||
UIDialogService,
|
||||
CornerstoneViewportService,
|
||||
SegmentationService,
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
UINotificationService,
|
||||
} = servicesManager.services;
|
||||
@ -133,34 +143,23 @@ const commandsModule = ({ servicesManager }) => {
|
||||
});
|
||||
viewport.render();
|
||||
},
|
||||
toggleCrosshairs({ toolGroupId, toggledState }) {
|
||||
const toolName = 'Crosshairs';
|
||||
// If it is Enabled
|
||||
if (toggledState) {
|
||||
actions.setToolActive({ toolName, toolGroupId });
|
||||
return;
|
||||
}
|
||||
const toolGroup = _getToolGroup(toolGroupId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroup.setToolDisabled(toolName);
|
||||
|
||||
// Get the primary toolId from the ToolBarService and set it to active
|
||||
// Since it was set to passive if not already active
|
||||
const primaryActiveTool = ToolBarService.state.primaryToolId;
|
||||
if (
|
||||
toolGroup?.toolOptions[primaryActiveTool]?.mode ===
|
||||
Enums.ToolModes.Passive
|
||||
) {
|
||||
toolGroup.setToolActive(primaryActiveTool, {
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
});
|
||||
}
|
||||
},
|
||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||
if (toolName === 'Crosshairs') {
|
||||
const activeViewportToolGroup = _getToolGroup(null);
|
||||
|
||||
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
|
||||
UINotificationService.show({
|
||||
title: 'Crosshairs',
|
||||
message:
|
||||
'You need to be in a MPR view to use Crosshairs. Click on MPR button in the toolbar to activate it.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
throw new Error('Crosshairs tool is not available in this viewport');
|
||||
}
|
||||
}
|
||||
|
||||
const toolGroup = _getToolGroup(toolGroupId);
|
||||
|
||||
if (!toolGroup) {
|
||||
@ -298,8 +297,12 @@ const commandsModule = ({ servicesManager }) => {
|
||||
if (viewport instanceof StackViewport) {
|
||||
viewport.resetProperties();
|
||||
viewport.resetCamera();
|
||||
viewport.render();
|
||||
} else {
|
||||
// Todo: add reset properties for volume viewport
|
||||
viewport.resetCamera();
|
||||
}
|
||||
|
||||
viewport.render();
|
||||
},
|
||||
scaleViewport: ({ direction }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
@ -331,59 +334,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
const { viewport } = enabledElement;
|
||||
const options = { delta: direction };
|
||||
|
||||
csToolsUtils.scroll(viewport, options);
|
||||
},
|
||||
async createSegmentationForDisplaySet({ displaySetInstanceUID }) {
|
||||
const volumeId = displaySetInstanceUID;
|
||||
|
||||
const segmentationUID = csUtils.uuidv4();
|
||||
const segmentationId = `${volumeId}::${segmentationUID}`;
|
||||
|
||||
await volumeLoader.createAndCacheDerivedVolume(volumeId, {
|
||||
volumeId: segmentationId,
|
||||
});
|
||||
|
||||
// Add the segmentations to state
|
||||
segmentation.addSegmentations([
|
||||
{
|
||||
segmentationId,
|
||||
representation: {
|
||||
// The type of segmentation
|
||||
type: Enums.SegmentationRepresentations.Labelmap,
|
||||
// The actual segmentation data, in the case of labelmap this is a
|
||||
// reference to the source volume of the segmentation.
|
||||
data: {
|
||||
volumeId: segmentationId,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
async addSegmentationRepresentationToToolGroup({
|
||||
segmentationId,
|
||||
toolGroupId,
|
||||
representationType,
|
||||
}) {
|
||||
// // Add the segmentation representation to the toolgroup
|
||||
await segmentation.addSegmentationRepresentations(toolGroupId, [
|
||||
{
|
||||
segmentationId,
|
||||
type: representationType,
|
||||
},
|
||||
]);
|
||||
},
|
||||
getLabelmapVolumes: ({ segmentations }) => {
|
||||
if (!segmentations || !segmentations.length) {
|
||||
segmentations = SegmentationService.getSegmentations();
|
||||
}
|
||||
|
||||
const labelmapVolumes = segmentations.map(segmentation => {
|
||||
return cache.getVolume(segmentation.id);
|
||||
});
|
||||
|
||||
return labelmapVolumes;
|
||||
cstUtils.scroll(viewport, options);
|
||||
},
|
||||
setViewportColormap: ({
|
||||
viewportIndex,
|
||||
@ -398,7 +349,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
const actorEntries = viewport.getActors();
|
||||
|
||||
const actorEntry = actorEntries.find(actorEntry => {
|
||||
return actorEntry.uid === displaySetInstanceUID;
|
||||
return actorEntry.uid.includes(displaySetInstanceUID);
|
||||
});
|
||||
|
||||
const { actor: volumeActor } = actorEntry;
|
||||
@ -423,6 +374,254 @@ const commandsModule = ({ servicesManager }) => {
|
||||
setHangingProtocol: ({ protocolId }) => {
|
||||
HangingProtocolService.setProtocol(protocolId);
|
||||
},
|
||||
toggleMPR: ({ toggledState }) => {
|
||||
const { activeViewportIndex, viewports } = ViewportGridService.getState();
|
||||
const viewportDisplaySetInstanceUIDs =
|
||||
viewports[activeViewportIndex].displaySetInstanceUIDs;
|
||||
|
||||
const errorCallback = error => {
|
||||
UINotificationService.show({
|
||||
title: 'Multiplanar reconstruction (MPR) ',
|
||||
message:
|
||||
'Cannot create MPR for this DisplaySet since it is not reconstructable.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
};
|
||||
|
||||
const cacheId = 'beforeMPR';
|
||||
if (toggledState) {
|
||||
ViewportGridService.setCachedLayout({
|
||||
cacheId,
|
||||
cachedLayout: ViewportGridService.getState(),
|
||||
});
|
||||
|
||||
const matchDetails = {
|
||||
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
|
||||
};
|
||||
|
||||
HangingProtocolService.setProtocol(
|
||||
MPR_TOOLGROUP_ID,
|
||||
matchDetails,
|
||||
errorCallback
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { cachedLayout } = ViewportGridService.getState();
|
||||
|
||||
if (!cachedLayout || !cachedLayout[cacheId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { viewports: cachedViewports, numRows, numCols } = cachedLayout[
|
||||
cacheId
|
||||
];
|
||||
|
||||
// Todo: The following assumes that when turning off MPR we are applying the default
|
||||
// protocol which might not be the one that was used before MPR was turned on
|
||||
// In order to properly implement this logic, we should modify the hanging protocol
|
||||
// upon layout change with layout selector, and cache and restore it when turning
|
||||
// MPR on and off
|
||||
const viewportStructure = getProtocolViewportStructureFromGridViewports({
|
||||
viewports: cachedViewports,
|
||||
numRows,
|
||||
numCols,
|
||||
});
|
||||
|
||||
const viewportSpecificMatch = cachedViewports.reduce(
|
||||
(acc, viewport, index) => {
|
||||
const {
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
} = viewport;
|
||||
|
||||
acc[index] = {
|
||||
displaySetInstanceUIDs,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const defaultProtocol = HangingProtocolService.getProtocolById('default');
|
||||
|
||||
// Todo: this assumes there is only one stage in the default protocol
|
||||
const defaultProtocolStage = defaultProtocol.stages[0];
|
||||
defaultProtocolStage.viewportStructure = viewportStructure;
|
||||
|
||||
const { primaryToolId } = ToolBarService.state;
|
||||
const mprToolGroup = _getToolGroup(MPR_TOOLGROUP_ID);
|
||||
// turn off crosshairs if it is on
|
||||
if (
|
||||
primaryToolId === 'Crosshairs' ||
|
||||
mprToolGroup.getToolInstance('Crosshairs')?.mode ===
|
||||
Enums.ToolModes.Active
|
||||
) {
|
||||
const toolGroup = _getToolGroup(MPR_TOOLGROUP_ID);
|
||||
toolGroup.setToolDisabled('Crosshairs');
|
||||
ToolBarService.recordInteraction({
|
||||
groupId: 'WindowLevel',
|
||||
itemId: 'WindowLevel',
|
||||
interactionType: 'tool',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolName: 'WindowLevel',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// clear segmentations if they exist
|
||||
removeToolGroupSegmentationRepresentations(MPR_TOOLGROUP_ID);
|
||||
|
||||
HangingProtocolService.setProtocol(
|
||||
'default',
|
||||
viewportSpecificMatch,
|
||||
error => {
|
||||
UINotificationService.show({
|
||||
title: 'Multiplanar reconstruction (MPR) ',
|
||||
message:
|
||||
'Something went wrong while trying to restore the previous layout.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
toggleStackImageSync: ({ toggledState }) => {
|
||||
if (!toggledState) {
|
||||
STACK_IMAGE_SYNC_GROUPS_INFO.forEach(syncGroupInfo => {
|
||||
const { viewports, synchronizerId } = syncGroupInfo;
|
||||
|
||||
viewports.forEach(({ viewportId, renderingEngineId }) => {
|
||||
SyncGroupService.removeViewportFromSyncGroup(
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
synchronizerId
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
STACK_IMAGE_SYNC_GROUPS_INFO = [];
|
||||
|
||||
// create synchronization groups and add viewports
|
||||
let { viewports } = ViewportGridService.getState();
|
||||
|
||||
// filter empty viewports
|
||||
viewports = viewports.filter(
|
||||
viewport =>
|
||||
viewport.displaySetInstanceUIDs &&
|
||||
viewport.displaySetInstanceUIDs.length
|
||||
);
|
||||
|
||||
// filter reconstructable viewports
|
||||
viewports = viewports.filter(viewport => {
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
|
||||
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (displaySet && displaySet.isReconstructable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const viewportsByOrientation = viewports.reduce((acc, viewport) => {
|
||||
const { viewportId, viewportType } = viewport.viewportOptions;
|
||||
|
||||
if (viewportType !== 'stack') {
|
||||
console.warn('Viewport is not a stack, cannot sync images yet');
|
||||
return acc;
|
||||
}
|
||||
|
||||
const { element } = CornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
);
|
||||
const { viewport: csViewport, renderingEngineId } = getEnabledElement(
|
||||
element
|
||||
);
|
||||
const { viewPlaneNormal } = csViewport.getCamera();
|
||||
|
||||
// Should we round here? I guess so, but not sure how much precision we need
|
||||
const orientation = viewPlaneNormal.map(v => Math.round(v)).join(',');
|
||||
|
||||
if (!acc[orientation]) {
|
||||
acc[orientation] = [];
|
||||
}
|
||||
|
||||
acc[orientation].push({ viewportId, renderingEngineId });
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// create synchronizer for each group
|
||||
Object.values(viewportsByOrientation).map(viewports => {
|
||||
let synchronizerId = viewports
|
||||
.map(({ viewportId }) => viewportId)
|
||||
.join(',');
|
||||
|
||||
synchronizerId = `imageSync_${synchronizerId}`;
|
||||
|
||||
calculateViewportRegistrations(viewports);
|
||||
|
||||
viewports.forEach(({ viewportId, renderingEngineId }) => {
|
||||
SyncGroupService.addViewportToSyncGroup(
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
{
|
||||
type: 'stackimage',
|
||||
id: synchronizerId,
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
STACK_IMAGE_SYNC_GROUPS_INFO.push({
|
||||
synchronizerId,
|
||||
viewports,
|
||||
});
|
||||
});
|
||||
},
|
||||
toggleReferenceLines: ({ toggledState }) => {
|
||||
const { activeViewportIndex } = ViewportGridService.getState();
|
||||
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||
activeViewportIndex
|
||||
);
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toggledState) {
|
||||
toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
|
||||
}
|
||||
|
||||
toolGroup.setToolConfiguration(
|
||||
ReferenceLinesTool.toolName,
|
||||
{
|
||||
sourceViewportId: viewportId,
|
||||
},
|
||||
true // overwrite
|
||||
);
|
||||
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@ -524,22 +723,6 @@ const commandsModule = ({ servicesManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createSegmentationForDisplaySet: {
|
||||
commandFn: actions.createSegmentationForDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
addSegmentationRepresentationToToolGroup: {
|
||||
commandFn: actions.addSegmentationRepresentationToToolGroup,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
|
||||
getLabelmapVolumes: {
|
||||
commandFn: actions.getLabelmapVolumes,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setViewportColormap: {
|
||||
commandFn: actions.setViewportColormap,
|
||||
storeContexts: [],
|
||||
@ -550,6 +733,21 @@ const commandsModule = ({ servicesManager }) => {
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleMPR: {
|
||||
commandFn: actions.toggleMPR,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleStackImageSync: {
|
||||
commandFn: actions.toggleStackImageSync,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleReferenceLines: {
|
||||
commandFn: actions.toggleReferenceLines,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,144 +1,140 @@
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const MPRHangingProtocolGenerator: Types.HangingProtocol.ProtocolGenerator = ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) => {
|
||||
const {
|
||||
ViewportGridService,
|
||||
UINotificationService,
|
||||
DisplaySetService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { activeViewportIndex, viewports } = ViewportGridService.getState();
|
||||
const viewportDisplaySetInstanceUIDs =
|
||||
viewports[activeViewportIndex].displaySetInstanceUIDs;
|
||||
|
||||
if (
|
||||
!viewportDisplaySetInstanceUIDs ||
|
||||
!viewportDisplaySetInstanceUIDs.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySetsToHang = viewportDisplaySetInstanceUIDs.map(
|
||||
displaySetInstanceUID => {
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
);
|
||||
|
||||
if (displaySetsToHang.some(ds => !ds.isReconstructable)) {
|
||||
UINotificationService.show({
|
||||
title: 'Multiplanar reconstruction (MPR) ',
|
||||
message:
|
||||
'Cannot create MPR for this series since it is not reconstructable.',
|
||||
type: 'warning',
|
||||
displayTime: 3000,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDisplaySets = {};
|
||||
|
||||
displaySetsToHang.forEach(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = displaySet;
|
||||
|
||||
matchingDisplaySets[displaySetInstanceUID] = {
|
||||
displaySetInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} as Types.HangingProtocol.DisplaySetMatchDetails;
|
||||
});
|
||||
|
||||
const hpViewports: Types.HangingProtocol.Viewport[] = [
|
||||
'axial',
|
||||
'sagittal',
|
||||
'coronal',
|
||||
].map(viewportOrientation => {
|
||||
return {
|
||||
viewportOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOrientation,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
const mpr = {
|
||||
id: 'mpr',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'mpr',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2022-10-04T19:22:08.894Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [],
|
||||
displaySetSelectors: {
|
||||
mprDisplaySet: {
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'mpr3Stage',
|
||||
name: 'mpr',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
layoutOptions: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
x: 1 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
x: 2 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
displaySets: viewportDisplaySetInstanceUIDs.map(displaySetInstanceUID => {
|
||||
return {
|
||||
id: displaySetInstanceUID,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const protocol = {
|
||||
id: 'mpr',
|
||||
displaySetSelectors: {},
|
||||
stages: [
|
||||
{
|
||||
id: 'mprStage',
|
||||
name: 'mpr',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
layoutOptions: [
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
x: 1 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
x: 2 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1,
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
viewports: hpViewports,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
protocol,
|
||||
matchingDisplaySets,
|
||||
};
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'mpr',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'mprDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
id: 'mpr',
|
||||
protocol: MPRHangingProtocolGenerator,
|
||||
protocol: mpr,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@ -7,18 +7,22 @@ import {
|
||||
imageRetrievalPoolManager,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||
import init from './init.js';
|
||||
import init from './init';
|
||||
import commandsModule from './commandsModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import ToolGroupService from './services/ToolGroupService';
|
||||
import SyncGroupService from './services/SyncGroupService';
|
||||
import SegmentationService from './services/SegmentationService';
|
||||
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
||||
|
||||
import { toolNames } from './initCornerstoneTools';
|
||||
import { getEnabledElement } from './state';
|
||||
import { getEnabledElement, reset as enabledElementReset } from './state';
|
||||
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||
import dicomLoaderService from './utils/dicomLoaderService';
|
||||
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
|
||||
import { id } from './id';
|
||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
@ -50,6 +54,9 @@ const cornerstoneExtension = {
|
||||
imageLoadPoolManager.clearRequestStack(type);
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
csWADOImageLoader.destroy();
|
||||
enabledElementReset();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -69,6 +76,9 @@ const cornerstoneExtension = {
|
||||
);
|
||||
servicesManager.registerService(ToolGroupService(servicesManager));
|
||||
servicesManager.registerService(SyncGroupService(servicesManager));
|
||||
servicesManager.registerService(SegmentationService(servicesManager));
|
||||
servicesManager.registerService(CornerstoneCacheService(servicesManager));
|
||||
|
||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||
},
|
||||
getHangingProtocolModule,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import React from 'react';
|
||||
import { ContextMenuMeasurements } from '@ohif/ui';
|
||||
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
@ -13,7 +14,7 @@ import {
|
||||
imageLoadPoolManager,
|
||||
Settings,
|
||||
} from '@cornerstonejs/core';
|
||||
import { Enums, utilities } from '@cornerstonejs/tools';
|
||||
import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools';
|
||||
import {
|
||||
cornerstoneStreamingImageVolumeLoader,
|
||||
sharedArrayBufferImageLoader,
|
||||
@ -27,7 +28,6 @@ import callInputDialog from './utils/callInputDialog';
|
||||
import initCineService from './initCineService';
|
||||
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||
import initSegmentationService from './initSegmentationService';
|
||||
|
||||
const cs3DToolsEvents = Enums.Events;
|
||||
|
||||
@ -59,25 +59,48 @@ export default async function init({
|
||||
|
||||
initCornerstoneTools();
|
||||
|
||||
Settings.getRuntimeSettings().set('useCursors', Boolean(appConfig.useCursors));
|
||||
Settings.getRuntimeSettings().set(
|
||||
'useCursors',
|
||||
Boolean(appConfig.useCursors)
|
||||
);
|
||||
|
||||
const {
|
||||
UserAuthenticationService,
|
||||
ToolGroupService,
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
CineService,
|
||||
CornerstoneViewportService,
|
||||
HangingProtocolService,
|
||||
ToolGroupService,
|
||||
SegmentationService,
|
||||
ViewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
window.SegmentationService = SegmentationService;
|
||||
window.DisplaySetService = DisplaySetService;
|
||||
window.services = servicesManager.services;
|
||||
|
||||
if (cornerstone.getShouldUseCPURendering()) {
|
||||
_showCPURenderingModal(UIModalService, HangingProtocolService);
|
||||
}
|
||||
|
||||
const labelmapRepresentation =
|
||||
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(
|
||||
labelmapRepresentation,
|
||||
{
|
||||
fillAlpha: 0.3,
|
||||
fillAlphaInactive: 0.2,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.65,
|
||||
}
|
||||
);
|
||||
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
volumeLoader.registerUnknownVolumeLoader(
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
volumeLoader.registerVolumeLoader(
|
||||
'cornerstoneStreamingImageVolume',
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
@ -114,8 +137,6 @@ export default async function init({
|
||||
CornerstoneViewportService
|
||||
);
|
||||
|
||||
initSegmentationService(SegmentationService, CornerstoneViewportService);
|
||||
|
||||
initCineService(CineService);
|
||||
|
||||
const _getDefaultPosition = event => ({
|
||||
@ -137,8 +158,8 @@ export default async function init({
|
||||
currentPoints.canvas
|
||||
);
|
||||
|
||||
let menuItems = [];
|
||||
if (nearbyToolData) {
|
||||
const menuItems = [];
|
||||
if (nearbyToolData && nearbyToolData.metadata.toolName !== 'Crosshairs') {
|
||||
defaultMenuItems.forEach(item => {
|
||||
item.value = nearbyToolData;
|
||||
item.element = element;
|
||||
@ -266,6 +287,32 @@ export default async function init({
|
||||
utilities.stackPrefetch.enable(element);
|
||||
};
|
||||
|
||||
const resetCrosshairs = evt => {
|
||||
const { element } = evt.detail;
|
||||
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(
|
||||
element
|
||||
);
|
||||
|
||||
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
if (!toolGroup || !toolGroup._toolInstances?.['Crosshairs']) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = toolGroup._toolInstances['Crosshairs'].mode;
|
||||
|
||||
if (mode === Enums.ToolModes.Active) {
|
||||
toolGroup.setToolActive('Crosshairs');
|
||||
} else if (mode === Enums.ToolModes.Passive) {
|
||||
toolGroup.setToolPassive('Crosshairs');
|
||||
} else if (mode === Enums.ToolModes.Enabled) {
|
||||
toolGroup.setToolEnabled('Crosshairs');
|
||||
}
|
||||
};
|
||||
|
||||
function elementEnabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
@ -274,6 +321,8 @@ export default async function init({
|
||||
contextMenuHandleClick
|
||||
);
|
||||
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.STACK_VIEWPORT_NEW_STACK,
|
||||
newStackCallback
|
||||
@ -281,16 +330,15 @@ export default async function init({
|
||||
}
|
||||
|
||||
function elementDisabledHandler(evt) {
|
||||
const { viewportId, element } = evt.detail;
|
||||
|
||||
const viewportInfo = CornerstoneViewportService.getViewportInfo(viewportId);
|
||||
ToolGroupService.disable(viewportInfo);
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.removeEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
contextMenuHandleClick
|
||||
);
|
||||
|
||||
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
// TODO - consider removing the callback when all elements are gone
|
||||
// eventTarget.removeEventListener(
|
||||
// EVENTS.STACK_VIEWPORT_NEW_STACK,
|
||||
@ -307,4 +355,74 @@ export default async function init({
|
||||
EVENTS.ELEMENT_DISABLED,
|
||||
elementDisabledHandler.bind(null)
|
||||
);
|
||||
|
||||
ViewportGridService.subscribe(
|
||||
ViewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
|
||||
({ viewportIndex }) => {
|
||||
const viewportId = `viewport-${viewportIndex}`;
|
||||
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if reference lines are active
|
||||
const referenceLinesEnabled =
|
||||
toolGroup._toolInstances?.['ReferenceLines'].mode ===
|
||||
Enums.ToolModes.Enabled;
|
||||
|
||||
if (!referenceLinesEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroup.setToolConfiguration(
|
||||
ReferenceLinesTool.toolName,
|
||||
{
|
||||
sourceViewportId: viewportId,
|
||||
},
|
||||
true // overwrite
|
||||
);
|
||||
|
||||
// make sure to set it to enabled again since we want to recalculate
|
||||
// the source-target lines
|
||||
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function CPUModal() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Your computer does not have enough GPU power to support the default GPU
|
||||
rendering mode. OHIF has switched to CPU rendering mode. Please note
|
||||
that CPU rendering does not support all features such as Volume
|
||||
Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function _showCPURenderingModal(UIModalService, HangingProtocolService) {
|
||||
const callback = progress => {
|
||||
if (progress === 100) {
|
||||
UIModalService.show({
|
||||
content: CPUModal,
|
||||
title: 'OHIF Fell Back to CPU Rendering',
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const { unsubscribe } = HangingProtocolService.subscribe(
|
||||
HangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
|
||||
({ progress }) => {
|
||||
const done = callback(progress);
|
||||
|
||||
if (done) {
|
||||
unsubscribe();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -12,6 +12,7 @@ import {
|
||||
BidirectionalTool,
|
||||
ArrowAnnotateTool,
|
||||
DragProbeTool,
|
||||
ProbeTool,
|
||||
AngleTool,
|
||||
MagnifyTool,
|
||||
CrosshairsTool,
|
||||
@ -19,6 +20,7 @@ import {
|
||||
init,
|
||||
addTool,
|
||||
annotation,
|
||||
ReferenceLinesTool,
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
export default function initCornerstoneTools(configuration = {}) {
|
||||
@ -28,6 +30,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(StackScrollMouseWheelTool);
|
||||
addTool(StackScrollTool);
|
||||
addTool(ZoomTool);
|
||||
addTool(ProbeTool);
|
||||
addTool(VolumeRotateMouseWheelTool);
|
||||
addTool(MIPJumpToClickTool);
|
||||
addTool(LengthTool);
|
||||
@ -40,6 +43,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(MagnifyTool);
|
||||
addTool(CrosshairsTool);
|
||||
addTool(SegmentationDisplayTool);
|
||||
addTool(ReferenceLinesTool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
@ -67,6 +71,7 @@ const toolNames = {
|
||||
MipJumpToClick: MIPJumpToClickTool.toolName,
|
||||
Length: LengthTool.toolName,
|
||||
DragProbe: DragProbeTool.toolName,
|
||||
Probe: ProbeTool.toolName,
|
||||
RectangleROI: RectangleROITool.toolName,
|
||||
EllipticalROI: EllipticalROITool.toolName,
|
||||
Bidirectional: BidirectionalTool.toolName,
|
||||
@ -74,6 +79,7 @@ const toolNames = {
|
||||
Magnify: MagnifyTool.toolName,
|
||||
Crosshairs: CrosshairsTool.toolName,
|
||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||
ReferenceLines: ReferenceLinesTool.toolName,
|
||||
};
|
||||
|
||||
export { toolNames };
|
||||
|
||||
@ -1,141 +0,0 @@
|
||||
import { eventTarget, cache, triggerEvent } from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
|
||||
import Labelmap from './utils/segmentationServiceMappings/Labelmap';
|
||||
|
||||
function initSegmentationService(
|
||||
SegmentationService,
|
||||
CornerstoneViewportService
|
||||
) {
|
||||
connectToolsToSegmentationService(
|
||||
SegmentationService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
|
||||
connectSegmentationServiceToTools(
|
||||
SegmentationService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
}
|
||||
|
||||
function connectToolsToSegmentationService(
|
||||
SegmentationService,
|
||||
CornerstoneViewportService
|
||||
) {
|
||||
connectSegmentationServiceToTools(
|
||||
SegmentationService,
|
||||
CornerstoneViewportService
|
||||
);
|
||||
const segmentationUpdated = csToolsEnums.Events.SEGMENTATION_MODIFIED;
|
||||
|
||||
eventTarget.addEventListener(segmentationUpdated, evt => {
|
||||
const { segmentationId } = evt.detail;
|
||||
const segmentationState = csTools.segmentation.state.getSegmentation(
|
||||
segmentationId
|
||||
);
|
||||
|
||||
if (!segmentationState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!Object.keys(segmentationState.representationData).includes(
|
||||
csToolsEnums.SegmentationRepresentations.Labelmap
|
||||
)
|
||||
) {
|
||||
throw new Error('Non-labelmap representations are not supported yet');
|
||||
}
|
||||
|
||||
// Todo: handle other representations when available in cornerstone3D
|
||||
const segmentationSchema = Labelmap.toSegmentation(segmentationState);
|
||||
|
||||
try {
|
||||
SegmentationService.addOrUpdateSegmentation(
|
||||
segmentationId,
|
||||
segmentationSchema
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to add/update segmentation ${segmentationId}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function connectSegmentationServiceToTools(
|
||||
SegmentationService,
|
||||
CornerstoneViewportService
|
||||
) {
|
||||
const {
|
||||
SEGMENTATION_UPDATED,
|
||||
SEGMENTATION_REMOVED,
|
||||
} = SegmentationService.EVENTS;
|
||||
|
||||
SegmentationService.subscribe(SEGMENTATION_REMOVED, ({ id }) => {
|
||||
// Todo: This should be from the configuration
|
||||
const removeFromCache = true;
|
||||
|
||||
const sourceSegState = csTools.segmentation.state.getSegmentation(id);
|
||||
|
||||
if (!sourceSegState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolGroupIds = csTools.segmentation.state.getToolGroupsWithSegmentation(
|
||||
id
|
||||
);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
const UIDsToRemove = [];
|
||||
segmentationRepresentations.forEach(representation => {
|
||||
if (representation.segmentationId === id) {
|
||||
UIDsToRemove.push(representation.segmentationRepresentationUID);
|
||||
}
|
||||
});
|
||||
|
||||
csTools.segmentation.removeSegmentationsFromToolGroup(
|
||||
toolGroupId,
|
||||
UIDsToRemove
|
||||
);
|
||||
});
|
||||
|
||||
// cleanup the segmentation state too
|
||||
csTools.segmentation.state.removeSegmentation(id);
|
||||
|
||||
if (removeFromCache) {
|
||||
cache.removeVolumeLoadObject(id);
|
||||
}
|
||||
});
|
||||
|
||||
SegmentationService.subscribe(
|
||||
SEGMENTATION_UPDATED,
|
||||
({ id, segmentation, notYetUpdatedAtSource }) => {
|
||||
if (notYetUpdatedAtSource === false) {
|
||||
return;
|
||||
}
|
||||
const { label, text } = segmentation;
|
||||
|
||||
const sourceSegmentation = csTools.segmentation.state.getSegmentation(id);
|
||||
|
||||
// Update the label in the source if necessary
|
||||
if (sourceSegmentation.label !== label) {
|
||||
sourceSegmentation.label = label;
|
||||
}
|
||||
|
||||
if (sourceSegmentation.text !== text) {
|
||||
sourceSegmentation.text = text;
|
||||
}
|
||||
|
||||
triggerEvent(eventTarget, csTools.Enums.Events.SEGMENTATION_MODIFIED, {
|
||||
segmentationId: id,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default initSegmentationService;
|
||||
@ -1,7 +1,9 @@
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import { volumeLoader } from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import cornerstoneWADOImageLoader, {
|
||||
webWorkerManager,
|
||||
} from 'cornerstone-wado-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { errorHandler } from '@ohif/core';
|
||||
|
||||
@ -80,3 +82,13 @@ export default function initWADOImageLoader(
|
||||
|
||||
initWebWorkers(appConfig);
|
||||
}
|
||||
|
||||
export function destroy() {
|
||||
// Note: we don't want to call .terminate on the webWorkerManager since
|
||||
// that resets the config
|
||||
const webWorkers = webWorkerManager.webWorkers;
|
||||
for (let i = 0; i < webWorkers.length; i++) {
|
||||
webWorkers[i].worker.terminate();
|
||||
}
|
||||
webWorkers.length = 0;
|
||||
}
|
||||
|
||||
@ -0,0 +1,252 @@
|
||||
import { cache as cs3DCache, Enums, volumeLoader } from '@cornerstonejs/core';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
|
||||
const VOLUME_IMAGE_LOADER_SCHEME = 'streaming-wadors';
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
|
||||
class CornerstoneCacheService {
|
||||
stackImageIds: Map<string, string[]> = new Map();
|
||||
volumeImageIds: Map<string, string[]> = new Map();
|
||||
|
||||
constructor(servicesManager) {
|
||||
this.servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
public getCacheSize() {
|
||||
return cs3DCache.getCacheSize();
|
||||
}
|
||||
|
||||
public getCacheFreeSpace() {
|
||||
return cs3DCache.getBytesAvailable();
|
||||
}
|
||||
|
||||
public async createViewportData(
|
||||
displaySets: unknown[],
|
||||
viewportOptions: Record<string, unknown>,
|
||||
dataSource: unknown,
|
||||
initialImageIndex?: number
|
||||
): Promise<StackViewportData | VolumeViewportData> {
|
||||
let viewportType = viewportOptions.viewportType as string;
|
||||
|
||||
// Todo: Since Cornerstone 3D currently doesn't support segmentation
|
||||
// on stack viewport, we should check if whether the the displaySets
|
||||
// that are about to be displayed are referenced in a segmentation
|
||||
// as a reference volume, if so, we should hang a volume viewport
|
||||
// instead of a stack viewport
|
||||
if (this._shouldRenderSegmentation(displaySets)) {
|
||||
viewportType = 'volume';
|
||||
|
||||
// update viewportOptions to reflect the new viewport type
|
||||
viewportOptions.viewportType = viewportType;
|
||||
}
|
||||
|
||||
const cs3DViewportType = getCornerstoneViewportType(viewportType);
|
||||
let viewportData: StackViewportData | VolumeViewportData;
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.STACK) {
|
||||
viewportData = await this._getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
||||
}
|
||||
|
||||
viewportData.viewportType = cs3DViewportType;
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
|
||||
public async invalidateViewportData(
|
||||
viewportData: VolumeViewportData,
|
||||
invalidatedDisplaySetInstanceUID: string,
|
||||
dataSource,
|
||||
DisplaySetService
|
||||
) {
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
throw new Error('Invalidation of StackViewport is not supported yet');
|
||||
}
|
||||
|
||||
// Todo: grab the volume and get the id from the viewport itself
|
||||
const volumeId = `${VOLUME_LOADER_SCHEME}:${invalidatedDisplaySetInstanceUID}`;
|
||||
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (volume) {
|
||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||
}
|
||||
|
||||
const displaySets = viewportData.data.map(({ displaySetInstanceUID }) =>
|
||||
DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
|
||||
);
|
||||
|
||||
const newViewportData = await this._getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
);
|
||||
|
||||
return newViewportData;
|
||||
}
|
||||
|
||||
private _getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
): StackViewportData {
|
||||
// For Stack Viewport we don't have fusion currently
|
||||
const displaySet = displaySets[0];
|
||||
|
||||
let stackImageIds = this.stackImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!stackImageIds) {
|
||||
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||
}
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||
|
||||
const StackViewportData: StackViewportData = {
|
||||
viewportType: Enums.ViewportType.STACK,
|
||||
data: {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
imageIds: stackImageIds,
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof initialImageIndex === 'number') {
|
||||
StackViewportData.data.initialImageIndex = initialImageIndex;
|
||||
}
|
||||
|
||||
return StackViewportData;
|
||||
}
|
||||
|
||||
private async _getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
): Promise<VolumeViewportData> {
|
||||
// Todo: Check the cache for multiple scenarios to see if we need to
|
||||
// decache the volume data from other viewports or not
|
||||
|
||||
const volumeData = [];
|
||||
|
||||
for (const displaySet of displaySets) {
|
||||
// Don't create volumes for the displaySets that have custom load
|
||||
// function (e.g., SEG, RT, since they rely on the reference volumes
|
||||
// and they take care of their own loading after they are created in their
|
||||
// getSOPClassHandler method
|
||||
|
||||
if (displaySet.load && displaySet.load instanceof Function) {
|
||||
const { UserAuthenticationService } = this.servicesManager.services;
|
||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||
await displaySet.load({ headers });
|
||||
|
||||
volumeData.push({
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
});
|
||||
|
||||
// Todo: do some cache check and empty the cache if needed
|
||||
continue;
|
||||
}
|
||||
|
||||
const volumeLoaderSchema =
|
||||
displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
|
||||
|
||||
const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
|
||||
|
||||
let volumeImageIds = this.volumeImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
let volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volumeImageIds || !volume) {
|
||||
volumeImageIds = this._getCornerstoneVolumeImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
|
||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||
imageIds: volumeImageIds,
|
||||
});
|
||||
|
||||
this.volumeImageIds.set(
|
||||
displaySet.displaySetInstanceUID,
|
||||
volumeImageIds
|
||||
);
|
||||
}
|
||||
|
||||
volumeData.push({
|
||||
StudyInstanceUID: displaySet.StudyInstanceUID,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
volume,
|
||||
volumeId,
|
||||
imageIds: volumeImageIds,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||
data: volumeData,
|
||||
};
|
||||
}
|
||||
|
||||
private _shouldRenderSegmentation(displaySets) {
|
||||
const { SegmentationService } = this.servicesManager.services;
|
||||
|
||||
const viewportDisplaySetInstanceUIDs = displaySets.map(
|
||||
({ displaySetInstanceUID }) => displaySetInstanceUID
|
||||
);
|
||||
|
||||
// check inside segmentations if any of them are referencing the displaySets
|
||||
// that are about to be displayed
|
||||
const segmentations = SegmentationService.getSegmentations();
|
||||
|
||||
for (const segmentation of segmentations) {
|
||||
const segDisplaySetInstanceUID = segmentation.displaySetInstanceUID;
|
||||
|
||||
const shouldDisplaySeg = SegmentationService.shouldRenderSegmentation(
|
||||
viewportDisplaySetInstanceUIDs,
|
||||
segDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
}
|
||||
|
||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||
const stackImageIds = this._getCornerstoneStackImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
|
||||
if (stackImageIds[0].startsWith('dicomfile')) {
|
||||
return stackImageIds;
|
||||
}
|
||||
|
||||
return stackImageIds.map(imageId => {
|
||||
const imageURI = utils.imageIdToURI(imageId);
|
||||
return `${VOLUME_IMAGE_LOADER_SCHEME}:${imageURI}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default CornerstoneCacheService;
|
||||
@ -0,0 +1,10 @@
|
||||
import CornerstoneCacheService from './CornerstoneCacheService';
|
||||
|
||||
export default function ExtendedCornerstoneCacheService(serviceManager) {
|
||||
return {
|
||||
name: 'CornerstoneCacheService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new CornerstoneCacheService(serviceManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,88 @@
|
||||
import { Enums as csToolsEnums, Types as cstTypes } from '@cornerstonejs/tools';
|
||||
import { Types } from '@cornerstonejs/core';
|
||||
|
||||
type SegmentationConfig = cstTypes.LabelmapTypes.LabelmapConfig & {
|
||||
renderInactiveSegmentations: boolean;
|
||||
brushSize: number;
|
||||
brushThresholdGate: number;
|
||||
};
|
||||
|
||||
type Segment = {
|
||||
// the label for the segment
|
||||
label: string;
|
||||
// the index of the segment in the segmentation
|
||||
segmentIndex: number;
|
||||
// the color of the segment
|
||||
color: Types.Point3;
|
||||
// the opacity of the segment
|
||||
opacity: number;
|
||||
// whether the segment is visible
|
||||
isVisible: boolean;
|
||||
// whether the segment is locked
|
||||
isLocked: boolean;
|
||||
};
|
||||
|
||||
type Segmentation = {
|
||||
// active segment index is the index of the segment that is currently being edited.
|
||||
activeSegmentIndex: number;
|
||||
// colorLUTIndex is the index of the color LUT that is currently being used.
|
||||
colorLUTIndex: number;
|
||||
// if segmentation contains any data (often calculated from labelmap)
|
||||
cachedStats: Record<string, any>;
|
||||
// displayText is the text that is displayed on the segmentation panel (often derived from the data)
|
||||
displayText?: string[];
|
||||
// the id of the segmentation
|
||||
id: string;
|
||||
// if the segmentation is the active segmentation being used in the viewer
|
||||
isActive: boolean;
|
||||
// if the segmentation is visible in the viewer
|
||||
isVisible: boolean;
|
||||
// the label of the segmentation
|
||||
label: string;
|
||||
// the number of segments in the segmentation
|
||||
segmentCount: number;
|
||||
// the array of segments with their details, [null, segment1, segment2, ...]
|
||||
segments: Array<Segment>;
|
||||
// the set of segments that are locked
|
||||
segmentsLocked: Array<number>;
|
||||
// the segmentation representation type
|
||||
type: csToolsEnums.SegmentationRepresentations;
|
||||
// if labelmap, the id of the volume that the labelmap is associated with
|
||||
volumeId?: string;
|
||||
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
|
||||
// but hydrated SEG -> segmentation that is persisted in the store)
|
||||
hydrated: boolean;
|
||||
};
|
||||
|
||||
// Schema to generate a segmentation
|
||||
type SegmentationSchema = {
|
||||
// active segment index for the segmentation
|
||||
activeSegmentIndex: number;
|
||||
// statistics that are derived from the segmentation
|
||||
cachedStats: Record<string, any>;
|
||||
// the displayText for the segmentation in the panels
|
||||
displayText?: string[];
|
||||
// segmentation id
|
||||
id: string;
|
||||
// displaySetInstanceUID
|
||||
displaySetInstanceUID: string;
|
||||
// segmentation label
|
||||
label: string;
|
||||
// segment indices that are locked for the segmentation
|
||||
segmentsLocked: Array<number>;
|
||||
// the type of the segmentation (e.g., Labelmap etc.)
|
||||
type: csToolsEnums.SegmentationRepresentations;
|
||||
// the volume id of the volume that the labelmap is associated with, this only exists for the labelmap representation
|
||||
volumeId: string;
|
||||
// the referenced volumeURI for the segmentation
|
||||
referencedVolumeURI: string;
|
||||
// whether the segmentation is hydrated or not (non-hydrated SEG -> temporary segmentation for display in SEG Viewport
|
||||
// but hydrated SEG -> segmentation that is persisted in the store)
|
||||
hydrated: boolean;
|
||||
// the number of segments in the segmentation
|
||||
segmentCount: number;
|
||||
// the array of segments with their details
|
||||
segments: Array<Segment>;
|
||||
};
|
||||
|
||||
export { SegmentationConfig, Segment, Segmentation, SegmentationSchema };
|
||||
@ -0,0 +1,10 @@
|
||||
import SegmentationService from './SegmentationService';
|
||||
|
||||
export default function ExtendedSegmentationService(servicesManager) {
|
||||
return {
|
||||
name: 'SegmentationService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new SegmentationService({ servicesManager });
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -31,6 +31,7 @@ export type SyncGroup = {
|
||||
const POSITION = 'cameraposition';
|
||||
const VOI = 'voi';
|
||||
const ZOOMPAN = 'zoompan';
|
||||
const STACKIMAGE = 'stackimage';
|
||||
|
||||
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
||||
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||
@ -43,6 +44,7 @@ export default class SyncGroupService {
|
||||
[POSITION]: synchronizers.createCameraPositionSynchronizer,
|
||||
[VOI]: synchronizers.createVOISynchronizer,
|
||||
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
|
||||
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
|
||||
};
|
||||
|
||||
constructor(serviceManager) {
|
||||
@ -72,7 +74,7 @@ export default class SyncGroupService {
|
||||
* @param creator
|
||||
*/
|
||||
public setSynchronizer(type: string, creator: SyncCreator): void {
|
||||
this.synchronizerCreators[type] = creator;
|
||||
this.synchronizerCreators[type.toLowerCase()] = creator;
|
||||
}
|
||||
|
||||
protected _getOrCreateSynchronizer(
|
||||
@ -91,20 +93,29 @@ export default class SyncGroupService {
|
||||
public addViewportToSyncGroup(
|
||||
viewportId: string,
|
||||
renderingEngineId: string,
|
||||
syncGroups?: (SyncGroup | string)[]
|
||||
syncGroups?: SyncGroup | string | SyncGroup[] | string[]
|
||||
): void {
|
||||
if (!syncGroups || !syncGroups.length) {
|
||||
if (!syncGroups) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncGroups.forEach(syncGroup => {
|
||||
const syncGroupsArray = Array.isArray(syncGroups)
|
||||
? syncGroups
|
||||
: [syncGroups];
|
||||
|
||||
syncGroupsArray.forEach(syncGroup => {
|
||||
const syncGroupObj = asSyncGroup(syncGroup);
|
||||
const { type, target = true, source = true, options = {} } = syncGroupObj;
|
||||
const { id = type } = syncGroupObj;
|
||||
const {
|
||||
type,
|
||||
target = true,
|
||||
source = true,
|
||||
options = {},
|
||||
id = type,
|
||||
} = syncGroupObj;
|
||||
|
||||
const synchronizer = this._getOrCreateSynchronizer(type, id, options);
|
||||
|
||||
synchronizer.setOptions(viewportId, options);
|
||||
|
||||
const viewportInfo = { viewportId, renderingEngineId };
|
||||
if (target && source) {
|
||||
synchronizer.add(viewportInfo);
|
||||
@ -123,11 +134,16 @@ export default class SyncGroupService {
|
||||
|
||||
public removeViewportFromSyncGroup(
|
||||
viewportId: string,
|
||||
renderingEngineId: string
|
||||
renderingEngineId: string,
|
||||
syncGroupId?: string
|
||||
): void {
|
||||
const synchronizers = SynchronizerManager.getAllSynchronizers();
|
||||
|
||||
synchronizers.forEach(synchronizer => {
|
||||
const filteredSynchronizers = syncGroupId
|
||||
? synchronizers.filter(s => s.id === syncGroupId)
|
||||
: synchronizers;
|
||||
|
||||
filteredSynchronizers.forEach(synchronizer => {
|
||||
if (!synchronizer) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -72,7 +72,16 @@ export default class ToolGroupService {
|
||||
this.toolGroupIds = new Set();
|
||||
}
|
||||
|
||||
public disable(viewportId: string, renderingEngineId: string): void {
|
||||
public destroyToolGroup(toolGroupId: string) {
|
||||
ToolGroupManager.destroyToolGroup(toolGroupId);
|
||||
this.toolGroupIds.delete(toolGroupId);
|
||||
}
|
||||
|
||||
public removeViewportFromToolGroup(
|
||||
viewportId: string,
|
||||
renderingEngineId: string,
|
||||
deleteToolGroupIfEmpty?: boolean
|
||||
): void {
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
renderingEngineId
|
||||
@ -85,9 +94,10 @@ export default class ToolGroupService {
|
||||
toolGroup.removeViewports(renderingEngineId, viewportId);
|
||||
|
||||
const viewportIds = toolGroup.getViewportIds();
|
||||
// if (viewportIds.length === 0) {
|
||||
// ToolGroupManager.destroyToolGroup(toolGroup.id);
|
||||
// }
|
||||
|
||||
if (viewportIds.length === 0 && deleteToolGroupIfEmpty) {
|
||||
ToolGroupManager.destroyToolGroup(toolGroup.id);
|
||||
}
|
||||
}
|
||||
|
||||
public addViewportToToolGroup(
|
||||
@ -110,7 +120,10 @@ export default class ToolGroupService {
|
||||
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||
}
|
||||
|
||||
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
|
||||
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, {
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
public createToolGroup(toolGroupId: string): Types.IToolGroup {
|
||||
@ -122,7 +135,9 @@ export default class ToolGroupService {
|
||||
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
|
||||
this.toolGroupIds.add(toolGroupId);
|
||||
|
||||
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
|
||||
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, {
|
||||
toolGroupId,
|
||||
});
|
||||
|
||||
return toolGroup;
|
||||
}
|
||||
@ -195,9 +210,11 @@ export default class ToolGroupService {
|
||||
|
||||
private _getToolNames(toolGroupTools: Tools): string[] {
|
||||
const toolNames = [];
|
||||
toolGroupTools.active.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
});
|
||||
if (toolGroupTools.active) {
|
||||
toolGroupTools.active.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
});
|
||||
}
|
||||
if (toolGroupTools.passive) {
|
||||
toolGroupTools.passive.forEach(tool => {
|
||||
toolNames.push(tool.toolName);
|
||||
@ -221,9 +238,12 @@ export default class ToolGroupService {
|
||||
|
||||
private _setToolsMode(toolGroup, tools) {
|
||||
const { active, passive, enabled, disabled } = tools;
|
||||
active.forEach(({ toolName, bindings }) => {
|
||||
toolGroup.setToolActive(toolName, { bindings });
|
||||
});
|
||||
|
||||
if (active) {
|
||||
active.forEach(({ toolName, bindings }) => {
|
||||
toolGroup.setToolActive(toolName, { bindings });
|
||||
});
|
||||
}
|
||||
|
||||
if (passive) {
|
||||
passive.forEach(({ toolName }) => {
|
||||
|
||||
@ -1,231 +0,0 @@
|
||||
import {
|
||||
cache as cs3DCache,
|
||||
Enums,
|
||||
Types,
|
||||
volumeLoader,
|
||||
} from '@cornerstonejs/core';
|
||||
import { utils, pubSubServiceInterface } from '@ohif/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
|
||||
export type StackData = {
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
imageIds: string[];
|
||||
frameRate?: number;
|
||||
isClip?: boolean;
|
||||
initialImageIndex?: number | string | null;
|
||||
viewportType: Enums.ViewportType;
|
||||
};
|
||||
|
||||
export type VolumeData = {
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUIDs: string[]; // can have more than one displaySet (fusion)
|
||||
imageIds: string[][]; // can have more than one imageId list (fusion)
|
||||
volumes: Types.IVolume[];
|
||||
viewportType: Enums.ViewportType;
|
||||
};
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'streaming-wadors';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_DATA_CHANGED: 'event::cornerstone::viewportdatachanged',
|
||||
};
|
||||
|
||||
export type IViewportData = StackData | VolumeData;
|
||||
|
||||
class CornerstoneCacheService {
|
||||
stackImageIds: Map<string, string[]> = new Map();
|
||||
volumeImageIds: Map<string, string[]> = new Map();
|
||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||
EVENTS: { [key: string]: string };
|
||||
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
public getCacheSize() {
|
||||
return cs3DCache.getCacheSize();
|
||||
}
|
||||
|
||||
public getCacheFreeSpace() {
|
||||
return cs3DCache.getBytesAvailable();
|
||||
}
|
||||
|
||||
public async getViewportData(
|
||||
viewportIndex: number,
|
||||
displaySets: unknown[],
|
||||
viewportType: string,
|
||||
dataSource: unknown,
|
||||
callback: (val: IViewportData) => unknown,
|
||||
initialImageIndex?: number
|
||||
): Promise<StackData | VolumeData> {
|
||||
const cs3DViewportType = getCornerstoneViewportType(viewportType);
|
||||
let viewportData: IViewportData;
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.STACK) {
|
||||
viewportData = await this._getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
||||
}
|
||||
|
||||
viewportData.viewportType = cs3DViewportType;
|
||||
|
||||
await callback(viewportData);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
});
|
||||
|
||||
return viewportData;
|
||||
}
|
||||
public async invalidateViewportData(
|
||||
viewportData: VolumeData,
|
||||
invalidatedDisplaySetInstanceUID: string,
|
||||
dataSource,
|
||||
DisplaySetService
|
||||
) {
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
throw new Error('Invalidation of StackViewport is not supported yet');
|
||||
}
|
||||
|
||||
const volumeId = invalidatedDisplaySetInstanceUID;
|
||||
const volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (volume) {
|
||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
||||
}
|
||||
|
||||
const displaySets = viewportData.displaySetInstanceUIDs.map(
|
||||
DisplaySetService.getDisplaySetByUID
|
||||
);
|
||||
|
||||
const newViewportData = await this._getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
);
|
||||
|
||||
return newViewportData;
|
||||
}
|
||||
|
||||
private _getStackViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
initialImageIndex
|
||||
): StackData {
|
||||
// For Stack Viewport we don't have fusion currently
|
||||
const displaySet = displaySets[0];
|
||||
|
||||
let stackImageIds = this.stackImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!stackImageIds) {
|
||||
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||
}
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
||||
|
||||
const stackData: StackData = {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
viewportType: Enums.ViewportType.STACK,
|
||||
imageIds: stackImageIds,
|
||||
};
|
||||
|
||||
if (typeof initialImageIndex === 'number') {
|
||||
stackData.initialImageIndex = initialImageIndex;
|
||||
}
|
||||
|
||||
return stackData;
|
||||
}
|
||||
|
||||
private async _getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets
|
||||
): Promise<VolumeData> {
|
||||
// Check the cache for multiple scenarios to see if we need to
|
||||
// decache the volume data from other viewports or not
|
||||
|
||||
const volumeImageIdsArray = [];
|
||||
const volumes = [];
|
||||
|
||||
for (const displaySet of displaySets) {
|
||||
const volumeId = displaySet.displaySetInstanceUID;
|
||||
|
||||
let volumeImageIds = this.volumeImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
let volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volumeImageIds || !volume) {
|
||||
volumeImageIds = this._getCornerstoneVolumeImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
|
||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||
imageIds: volumeImageIds,
|
||||
});
|
||||
|
||||
this.volumeImageIds.set(
|
||||
displaySet.displaySetInstanceUID,
|
||||
volumeImageIds
|
||||
);
|
||||
}
|
||||
|
||||
volumeImageIdsArray.push(volumeImageIds);
|
||||
volumes.push(volume);
|
||||
}
|
||||
|
||||
// assert displaySets are from the same study
|
||||
const { StudyInstanceUID } = displaySets[0];
|
||||
const displaySetInstanceUIDs = [];
|
||||
|
||||
displaySets.forEach(displaySet => {
|
||||
if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
|
||||
throw new Error('Display sets are not from the same study');
|
||||
}
|
||||
|
||||
displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
|
||||
});
|
||||
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUIDs,
|
||||
imageIds: volumeImageIdsArray,
|
||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
||||
volumes,
|
||||
};
|
||||
}
|
||||
|
||||
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
}
|
||||
|
||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||
const stackImageIds = this._getCornerstoneStackImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
|
||||
return stackImageIds.map(imageId => {
|
||||
const imageURI = utils.imageIdToURI(imageId);
|
||||
return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CacheService = new CornerstoneCacheService();
|
||||
export default CacheService;
|
||||
@ -2,7 +2,6 @@ import { pubSubServiceInterface } from '@ohif/core';
|
||||
import {
|
||||
RenderingEngine,
|
||||
StackViewport,
|
||||
Enums,
|
||||
Types,
|
||||
getRenderingEngine,
|
||||
utilities as csUtils,
|
||||
@ -18,7 +17,10 @@ import ViewportInfo, {
|
||||
DisplaySetOptions,
|
||||
PublicViewportOptions,
|
||||
} from './Viewport';
|
||||
import { StackData, VolumeData } from './CornerstoneCacheService';
|
||||
import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
import {
|
||||
setColormap,
|
||||
setLowerUpperColorTransferFunction,
|
||||
@ -27,8 +29,8 @@ import {
|
||||
import JumpPresets from '../../utils/JumpPresets';
|
||||
|
||||
const EVENTS = {
|
||||
VIEWPORT_INFO_CREATED:
|
||||
'event::cornerstone::viewportservice:viewportinfocreated',
|
||||
VIEWPORT_DATA_CHANGED:
|
||||
'event::cornerstoneViewportService:viewportDataChanged',
|
||||
};
|
||||
|
||||
/**
|
||||
@ -39,8 +41,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
renderingEngine: Types.IRenderingEngine | null;
|
||||
viewportsInfo: Map<number, ViewportInfo>;
|
||||
viewportGridResizeObserver: ResizeObserver | null;
|
||||
// TODO - get the right type here.
|
||||
hangingProtocolService: object;
|
||||
viewportsDisplaySets: Map<string, string[]> = new Map();
|
||||
|
||||
/**
|
||||
* Service-specific
|
||||
@ -52,6 +53,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
enableResizeDetector: true;
|
||||
resizeRefreshRateMs: 200;
|
||||
resizeRefreshMode: 'debounce';
|
||||
servicesManager = null;
|
||||
|
||||
constructor(servicesManager) {
|
||||
this.renderingEngine = null;
|
||||
@ -60,8 +62,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
//
|
||||
this.listeners = {};
|
||||
this.EVENTS = EVENTS;
|
||||
const { HangingProtocolService } = servicesManager.services;
|
||||
this.hangingProtocolService = HangingProtocolService;
|
||||
this.servicesManager = servicesManager;
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
//
|
||||
}
|
||||
@ -71,7 +72,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} elementRef
|
||||
*/
|
||||
public enableElement(
|
||||
public enableViewport(
|
||||
viewportIndex: number,
|
||||
viewportOptions: PublicViewportOptions,
|
||||
elementRef: HTMLDivElement
|
||||
@ -87,6 +88,16 @@ class CornerstoneViewportService implements IViewportService {
|
||||
return `viewport-${viewportIndex}`;
|
||||
}
|
||||
|
||||
public getViewportIds(): string[] {
|
||||
const viewportIds = [];
|
||||
|
||||
this.viewportsInfo.forEach(viewportInfo => {
|
||||
viewportIds.push(viewportInfo.getViewportId());
|
||||
});
|
||||
|
||||
return viewportIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* It retrieves the renderingEngine if it does exist, or creates one otherwise
|
||||
* @returns {RenderingEngine} rendering engine
|
||||
@ -112,10 +123,9 @@ class CornerstoneViewportService implements IViewportService {
|
||||
*/
|
||||
public resize() {
|
||||
const immediate = true;
|
||||
const resetPan = false;
|
||||
const resetZoom = false;
|
||||
const keepCamera = true;
|
||||
|
||||
this.renderingEngine.resize(immediate, resetPan, resetZoom);
|
||||
this.renderingEngine.resize(immediate, keepCamera);
|
||||
this.renderingEngine.render();
|
||||
}
|
||||
|
||||
@ -126,8 +136,9 @@ class CornerstoneViewportService implements IViewportService {
|
||||
this._removeResizeObserver();
|
||||
this.viewportGridResizeObserver = null;
|
||||
this.renderingEngine.destroy();
|
||||
this.viewportsDisplaySets.clear();
|
||||
this.renderingEngine = null;
|
||||
cache.purgeVolumeCache();
|
||||
cache.purgeCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -142,12 +153,11 @@ class CornerstoneViewportService implements IViewportService {
|
||||
}
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
this.renderingEngine.disableElement(viewportId);
|
||||
this.viewportsInfo.delete(viewportIndex);
|
||||
|
||||
if (this.viewportsInfo.size === 0) {
|
||||
this.destroy();
|
||||
}
|
||||
this.renderingEngine && this.renderingEngine.disableElement(viewportId);
|
||||
|
||||
this.viewportsInfo.get(viewportIndex).destroy();
|
||||
this.viewportsInfo.delete(viewportIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -158,9 +168,9 @@ class CornerstoneViewportService implements IViewportService {
|
||||
* @param {*} dataSource
|
||||
* @returns
|
||||
*/
|
||||
public setViewportDisplaySets(
|
||||
public setViewportData(
|
||||
viewportIndex: number,
|
||||
viewportData: StackData | VolumeData,
|
||||
viewportData: StackViewportData | VolumeViewportData,
|
||||
publicViewportOptions: PublicViewportOptions,
|
||||
publicDisplaySetOptions: DisplaySetOptions[]
|
||||
): void {
|
||||
@ -179,8 +189,12 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
viewportInfo.setViewportOptions(viewportOptions);
|
||||
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
||||
viewportInfo.setViewportData(viewportData);
|
||||
|
||||
this._broadcastEvent(EVENTS.VIEWPORT_INFO_CREATED, viewportInfo);
|
||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
});
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const element = viewportInfo.getElement();
|
||||
@ -203,6 +217,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
// renderingEngine is designed to be used like this. This will trigger
|
||||
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
|
||||
this._setDisplaySets(viewportId, viewportData, viewportInfo);
|
||||
}
|
||||
|
||||
@ -265,11 +280,18 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
_setStackViewport(
|
||||
viewport: Types.IStackViewport,
|
||||
viewportData: StackData,
|
||||
viewportData: StackViewportData,
|
||||
viewportInfo: ViewportInfo
|
||||
) {
|
||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||
const { imageIds, initialImageIndex } = viewportData;
|
||||
|
||||
const {
|
||||
imageIds,
|
||||
initialImageIndex,
|
||||
displaySetInstanceUID,
|
||||
} = viewportData.data;
|
||||
|
||||
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
|
||||
|
||||
let initialImageIndexToUse = initialImageIndex;
|
||||
|
||||
@ -331,7 +353,15 @@ class CornerstoneViewportService implements IViewportService {
|
||||
}
|
||||
|
||||
if (preset === JumpPresets.Middle) {
|
||||
return Math.floor(lastSliceIndex / 2);
|
||||
// Note: this is a simple but yet very important formula.
|
||||
// since viewport reset works with the middle slice
|
||||
// if the below formula is not correct, on a viewport reset
|
||||
// it will jump to a different slice than the middle one which
|
||||
// was the initial slice, and we have some tools such as Crosshairs
|
||||
// which rely on a relative camera modifications and those will break.
|
||||
return lastSliceIndex % 2 === 0
|
||||
? lastSliceIndex / 2
|
||||
: (lastSliceIndex + 1) / 2;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@ -339,7 +369,7 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
async _setVolumeViewport(
|
||||
viewport: Types.IVolumeViewport,
|
||||
viewportData: VolumeData,
|
||||
viewportData: VolumeViewportData,
|
||||
viewportInfo: ViewportInfo
|
||||
): Promise<void> {
|
||||
// TODO: We need to overhaul the way data sources work so requests can be made
|
||||
@ -355,21 +385,24 @@ class CornerstoneViewportService implements IViewportService {
|
||||
// (This call may or may not create sub-requests for series metadata)
|
||||
const volumeInputArray = [];
|
||||
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
||||
const { hangingProtocolService } = this;
|
||||
const { HangingProtocolService } = this.servicesManager.services;
|
||||
|
||||
for (let i = 0; i < viewportData.imageIds.length; i++) {
|
||||
const imageIds = viewportData.imageIds[i];
|
||||
const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
|
||||
const displaySetOptions = displaySetOptionsArray[i];
|
||||
const volumeToLoad = [];
|
||||
const displaySetInstanceUIDs = [];
|
||||
|
||||
const volumeId = displaySetInstanceUID;
|
||||
for (const [index, data] of viewportData.data.entries()) {
|
||||
const { volume, imageIds, displaySetInstanceUID } = data;
|
||||
|
||||
// if (displaySet.needsRerendering) {
|
||||
// console.warn('Removing volume from cache', volumeId);
|
||||
// cache.removeVolumeLoadObject(volumeId);
|
||||
// displaySet.needsRerendering = false;
|
||||
// this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
|
||||
// }
|
||||
displaySetInstanceUIDs.push(displaySetInstanceUID);
|
||||
|
||||
if (!volume) {
|
||||
continue;
|
||||
}
|
||||
|
||||
volumeToLoad.push(volume);
|
||||
|
||||
const displaySetOptions = displaySetOptionsArray[index];
|
||||
const { volumeId } = volume;
|
||||
|
||||
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
||||
|
||||
@ -386,18 +419,20 @@ class CornerstoneViewportService implements IViewportService {
|
||||
});
|
||||
}
|
||||
|
||||
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
|
||||
|
||||
if (
|
||||
hangingProtocolService.hasCustomImageLoadStrategy() &&
|
||||
!hangingProtocolService.customImageLoadPerformed
|
||||
HangingProtocolService.hasCustomImageLoadStrategy() &&
|
||||
!HangingProtocolService.customImageLoadPerformed
|
||||
) {
|
||||
// delegate the volume loading to the hanging protocol service if it has a custom image load strategy
|
||||
return hangingProtocolService.runImageLoadStrategy({
|
||||
return HangingProtocolService.runImageLoadStrategy({
|
||||
viewportId: viewport.id,
|
||||
volumeInputArray,
|
||||
});
|
||||
}
|
||||
|
||||
viewportData.volumes.forEach(volume => {
|
||||
volumeToLoad.forEach(volume => {
|
||||
volume.load();
|
||||
});
|
||||
|
||||
@ -406,9 +441,98 @@ class CornerstoneViewportService implements IViewportService {
|
||||
}
|
||||
|
||||
public async setVolumesForViewport(viewport, volumeInputArray) {
|
||||
const {
|
||||
DisplaySetService,
|
||||
SegmentationService,
|
||||
ToolGroupService,
|
||||
} = this.servicesManager.services;
|
||||
|
||||
await viewport.setVolumes(volumeInputArray);
|
||||
|
||||
// load any secondary displaySets
|
||||
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
|
||||
|
||||
const segDisplaySet = displaySetInstanceUIDs
|
||||
.map(DisplaySetService.getDisplaySetByUID)
|
||||
.find(displaySet => displaySet && displaySet.Modality === 'SEG');
|
||||
|
||||
if (segDisplaySet) {
|
||||
const { referencedVolumeId } = segDisplaySet;
|
||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
|
||||
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
|
||||
|
||||
if (referencedVolume) {
|
||||
SegmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentationId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
|
||||
const toolGroupSegmentationRepresentations =
|
||||
SegmentationService.getSegmentationRepresentationsForToolGroup(
|
||||
toolGroup.id
|
||||
) || [];
|
||||
|
||||
// csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||
// If the displaySet is not a SEG displaySet we assume it is a primary displaySet
|
||||
// and we can look into hydrated segmentations to check if any of them are
|
||||
// associated with the primary displaySet
|
||||
// get segmentations only returns the hydrated segmentations
|
||||
const segmentations = SegmentationService.getSegmentations();
|
||||
|
||||
for (const segmentation of segmentations) {
|
||||
// if there is already a segmentation representation for this segmentation
|
||||
// for this toolGroup, don't bother at all
|
||||
if (
|
||||
toolGroupSegmentationRepresentations.find(
|
||||
representation => representation.segmentationId === segmentation.id
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// otherwise, check if the hydrated segmentations are in the same FOR
|
||||
// as the primary displaySet, if so add the representation (since it was not there)
|
||||
const { id: segDisplaySetInstanceUID } = segmentation;
|
||||
|
||||
const segFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||
segDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
let shouldDisplaySeg = false;
|
||||
|
||||
for (const displaySetInstanceUID of displaySetInstanceUIDs) {
|
||||
const primaryFrameOfReferenceUID = this._getFrameOfReferenceUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (segFrameOfReferenceUID === primaryFrameOfReferenceUID) {
|
||||
shouldDisplaySeg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
const toolGroup = ToolGroupService.getToolGroupForViewport(
|
||||
viewport.id
|
||||
);
|
||||
|
||||
SegmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroup.id,
|
||||
segmentation.id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viewportInfo = this.getViewportInfo(viewport.id);
|
||||
|
||||
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
|
||||
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||
|
||||
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||
|
||||
if (
|
||||
@ -493,17 +617,21 @@ class CornerstoneViewportService implements IViewportService {
|
||||
|
||||
_setDisplaySets(
|
||||
viewportId: string,
|
||||
viewportData: StackData | VolumeData,
|
||||
viewportData: StackViewportData | VolumeViewportData,
|
||||
viewportInfo: ViewportInfo
|
||||
): void {
|
||||
const viewport = this.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
|
||||
this._setStackViewport(
|
||||
viewport,
|
||||
viewportData as StackViewportData,
|
||||
viewportInfo
|
||||
);
|
||||
} else if (viewport instanceof VolumeViewport) {
|
||||
this._setVolumeViewport(
|
||||
viewport,
|
||||
viewportData as VolumeData,
|
||||
viewportData as VolumeViewportData,
|
||||
viewportInfo
|
||||
);
|
||||
} else {
|
||||
@ -559,6 +687,10 @@ class CornerstoneViewportService implements IViewportService {
|
||||
} {
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
|
||||
if (!publicViewportOptions.viewportId) {
|
||||
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
|
||||
}
|
||||
|
||||
// Creating a temporary viewportInfo to handle defaults
|
||||
const newViewportInfo = new ViewportInfo(
|
||||
viewportIndex,
|
||||
@ -578,6 +710,31 @@ class CornerstoneViewportService implements IViewportService {
|
||||
displaySetOptions: newDisplaySetOptions,
|
||||
};
|
||||
}
|
||||
|
||||
_getFrameOfReferenceUID(displaySetInstanceUID) {
|
||||
const { DisplaySetService } = this.servicesManager.services;
|
||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (displaySet.frameOfReferenceUID) {
|
||||
return displaySet.frameOfReferenceUID;
|
||||
}
|
||||
|
||||
if (displaySet.Modality === 'SEG') {
|
||||
const { instance } = displaySet;
|
||||
return instance.FrameOfReferenceUID;
|
||||
}
|
||||
|
||||
const { images } = displaySet;
|
||||
if (images && images.length) {
|
||||
return images[0].FrameOfReferenceUID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ExtendedCornerstoneViewportService(serviceManager) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Types } from '@cornerstonejs/core';
|
||||
import { StackData, VolumeData } from './CornerstoneCacheService';
|
||||
import { StackData, VolumeData } from '../../types/CornerstoneCacheService';
|
||||
import {
|
||||
DisplaySetOptions,
|
||||
PublicViewportOptions,
|
||||
@ -31,7 +31,7 @@ export interface IViewportService {
|
||||
* @param {*} viewportIndex
|
||||
* @param {*} elementRef
|
||||
*/
|
||||
enableElement(
|
||||
enableViewport(
|
||||
viewportIndex: number,
|
||||
viewportOptions: ViewportOptions,
|
||||
elementRef: HTMLDivElement
|
||||
@ -65,7 +65,7 @@ export interface IViewportService {
|
||||
* @param {*} dataSource
|
||||
* @returns
|
||||
*/
|
||||
setViewportDisplaySets(
|
||||
setViewportData(
|
||||
viewportIndex: number,
|
||||
viewportData: StackData | VolumeData,
|
||||
publicViewportOptions: PublicViewportOptions,
|
||||
|
||||
@ -4,6 +4,10 @@ import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import JumpPresets from '../../utils/JumpPresets';
|
||||
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
||||
import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from './CornerstoneCacheService';
|
||||
|
||||
export type InitialImageOptions = {
|
||||
index?: number;
|
||||
@ -67,6 +71,7 @@ class ViewportInfo {
|
||||
private element: HTMLDivElement;
|
||||
private viewportOptions: ViewportOptions;
|
||||
private displaySetOptions: Array<DisplaySetOptions>;
|
||||
private viewportData: StackViewportData | VolumeViewportData;
|
||||
private renderingEngineId: string;
|
||||
|
||||
constructor(viewportIndex: number, viewportId: string) {
|
||||
@ -76,6 +81,13 @@ class ViewportInfo {
|
||||
this.setPublicDisplaySetOptions([{}]);
|
||||
}
|
||||
|
||||
public destroy = (): void => {
|
||||
this.element = null;
|
||||
this.viewportData = null;
|
||||
this.viewportOptions = null;
|
||||
this.displaySetOptions = null;
|
||||
};
|
||||
|
||||
public setRenderingEngineId(renderingEngineId: string): void {
|
||||
this.renderingEngineId = renderingEngineId;
|
||||
}
|
||||
@ -95,6 +107,16 @@ class ViewportInfo {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
public setViewportData(
|
||||
viewportData: StackViewportData | VolumeViewportData
|
||||
): void {
|
||||
this.viewportData = viewportData;
|
||||
}
|
||||
|
||||
public getViewportData(): StackViewportData | VolumeViewportData {
|
||||
return this.viewportData;
|
||||
}
|
||||
|
||||
public getViewportIndex(): number {
|
||||
return this.viewportIndex;
|
||||
}
|
||||
@ -118,6 +140,23 @@ class ViewportInfo {
|
||||
this.setDisplaySetOptions(displaySetOptions);
|
||||
}
|
||||
|
||||
public hasDisplaySet(displaySetInstanceUID: string): boolean {
|
||||
// Todo: currently this does not work for non image & referenceImage displaySets.
|
||||
// Since SEG and other derived displaySets are loaded in a different way, and not
|
||||
// via cornerstoneViewportService
|
||||
let viewportData = this.getViewportData();
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
viewportData = viewportData as VolumeViewportData;
|
||||
return viewportData.data.some(
|
||||
({ displaySetInstanceUID: dsUID }) => dsUID === displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
viewportData = viewportData as StackViewportData;
|
||||
return viewportData.data.displaySetInstanceUID === displaySetInstanceUID;
|
||||
}
|
||||
|
||||
public setPublicViewportOptions(
|
||||
viewportOptionsEntry: PublicViewportOptions
|
||||
): void {
|
||||
@ -201,12 +240,21 @@ class ViewportInfo {
|
||||
const displaySetOptions: Array<DisplaySetOptions> = [];
|
||||
|
||||
publicDisplaySetOptions.forEach(option => {
|
||||
if (!option) {
|
||||
option = {
|
||||
blendMode: undefined,
|
||||
slabThickness: undefined,
|
||||
colormap: undefined,
|
||||
voi: {},
|
||||
voiInverted: false,
|
||||
};
|
||||
}
|
||||
const blendMode = getCornerstoneBlendMode(option.blendMode);
|
||||
|
||||
displaySetOptions.push({
|
||||
voi: option.voi || ({} as VOI),
|
||||
voiInverted: option.voiInverted || false,
|
||||
colormap: option.colormap || undefined,
|
||||
voi: option.voi,
|
||||
voiInverted: option.voiInverted,
|
||||
colormap: option.colormap,
|
||||
slabThickness: option.slabThickness,
|
||||
blendMode,
|
||||
});
|
||||
|
||||
@ -31,4 +31,8 @@ const getEnabledElement = viewportIndex => {
|
||||
return state.enabledElements[viewportIndex];
|
||||
};
|
||||
|
||||
export { setEnabledElement, getEnabledElement };
|
||||
const reset = () => {
|
||||
state.enabledElements = {};
|
||||
};
|
||||
|
||||
export { setEnabledElement, getEnabledElement, reset };
|
||||
|
||||
38
extensions/cornerstone/src/types/CornerstoneCacheService.ts
Normal file
38
extensions/cornerstone/src/types/CornerstoneCacheService.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {
|
||||
Enums,
|
||||
Types,
|
||||
} from '@cornerstonejs/core';
|
||||
|
||||
|
||||
type StackData = {
|
||||
StudyInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
imageIds: string[];
|
||||
frameRate?: number;
|
||||
isClip?: boolean;
|
||||
initialImageIndex?: number | string | null;
|
||||
};
|
||||
|
||||
type VolumeData = {
|
||||
studyInstanceUID: string;
|
||||
displaySetInstanceUID: string;
|
||||
volume?: Types.IVolume;
|
||||
imageIds?: string[];
|
||||
};
|
||||
|
||||
type StackViewportData = {
|
||||
viewportType: Enums.ViewportType;
|
||||
data: StackData;
|
||||
};
|
||||
|
||||
type VolumeViewportData = {
|
||||
viewportType: Enums.ViewportType;
|
||||
data: VolumeData[];
|
||||
};
|
||||
|
||||
export type {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
StackData,
|
||||
VolumeData,
|
||||
};
|
||||
5
extensions/cornerstone/src/types/index.ts
Normal file
5
extensions/cornerstone/src/types/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as CornerstoneCacheService from './CornerstoneCacheService'
|
||||
|
||||
export type {
|
||||
CornerstoneCacheService
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import domtoimage from 'dom-to-image';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import html2canvas from 'html2canvas';
|
||||
import {
|
||||
Enums,
|
||||
getEnabledElement,
|
||||
getOrCreateCanvas,
|
||||
StackViewport,
|
||||
VolumeViewport,
|
||||
} from '@cornerstonejs/core';
|
||||
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||
import PropTypes from 'prop-types';
|
||||
@ -17,7 +17,6 @@ const MINIMUM_SIZE = 100;
|
||||
const DEFAULT_SIZE = 512;
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
|
||||
const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup';
|
||||
|
||||
const CornerstoneViewportDownloadForm = ({
|
||||
onClose,
|
||||
@ -26,15 +25,57 @@ const CornerstoneViewportDownloadForm = ({
|
||||
}) => {
|
||||
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
|
||||
const activeViewportElement = enabledElement?.element;
|
||||
const activeViewportEnabledElement = getEnabledElement(activeViewportElement);
|
||||
|
||||
const {
|
||||
viewportId: activeViewportId,
|
||||
renderingEngineId,
|
||||
} = activeViewportEnabledElement;
|
||||
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||
activeViewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
const toolModeAndBindings = Object.keys(toolGroup.toolOptions).reduce(
|
||||
(acc, toolName) => {
|
||||
const tool = toolGroup.toolOptions[toolName];
|
||||
const { mode, bindings } = tool;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[toolName]: {
|
||||
mode,
|
||||
bindings,
|
||||
},
|
||||
};
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
Object.keys(toolModeAndBindings).forEach(toolName => {
|
||||
const { mode, bindings } = toolModeAndBindings[toolName];
|
||||
toolGroup.setToolMode(toolName, mode, { bindings });
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const enableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||
const { renderingEngine, viewport } = getEnabledElement(
|
||||
activeViewportElement
|
||||
);
|
||||
|
||||
const viewportInput = {
|
||||
viewportId: VIEWPORT_ID,
|
||||
element: viewportElement,
|
||||
type: Enums.ViewportType.STACK,
|
||||
type: viewport.type,
|
||||
defaultOptions: {
|
||||
background: viewport.defaultOptions.background,
|
||||
orientation: viewport.defaultOptions.orientation,
|
||||
},
|
||||
};
|
||||
|
||||
renderingEngine.enableElement(viewportInput);
|
||||
@ -43,11 +84,9 @@ const CornerstoneViewportDownloadForm = ({
|
||||
|
||||
const disableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||
|
||||
const { renderingEngine } = getEnabledElement(viewportElement);
|
||||
return new Promise(resolve => {
|
||||
renderingEngine.disableElement(VIEWPORT_ID);
|
||||
ToolGroupManager.destroyToolGroup(TOOLGROUP_ID);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -115,26 +154,39 @@ const CornerstoneViewportDownloadForm = ({
|
||||
|
||||
const { viewport } = activeViewportEnabledElement;
|
||||
|
||||
if (!(viewport instanceof StackViewport)) {
|
||||
throw new Error('Viewport is not a StackViewport');
|
||||
}
|
||||
|
||||
const imageId = viewport.getCurrentImageId();
|
||||
|
||||
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||
const downloadViewport = renderingEngine.getViewport(
|
||||
VIEWPORT_ID
|
||||
) as StackViewport;
|
||||
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
|
||||
|
||||
downloadViewport.setStack([imageId]).then(() => {
|
||||
if (downloadViewport instanceof StackViewport) {
|
||||
const imageId = viewport.getCurrentImageId();
|
||||
const properties = viewport.getProperties();
|
||||
downloadViewport.setProperties(properties);
|
||||
|
||||
downloadViewport.setStack([imageId]).then(() => {
|
||||
downloadViewport.setProperties(properties);
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(
|
||||
height || image.height,
|
||||
MAX_TEXTURE_SIZE
|
||||
);
|
||||
|
||||
resolve({ width: newWidth, height: newHeight });
|
||||
});
|
||||
} else if (downloadViewport instanceof VolumeViewport) {
|
||||
const actors = viewport.getActors();
|
||||
// downloadViewport.setActors(actors);
|
||||
actors.forEach(actor => {
|
||||
downloadViewport.addActor(actor);
|
||||
});
|
||||
|
||||
downloadViewport.setCamera(viewport.getCamera());
|
||||
downloadViewport.render();
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||
|
||||
resolve({ width: newWidth, height: newHeight });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -164,31 +216,20 @@ const CornerstoneViewportDownloadForm = ({
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
let downloadToolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||
downloadViewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
// add the viewport to the toolGroup
|
||||
toolGroup.addViewport(downloadViewportId);
|
||||
|
||||
if (downloadToolGroup === undefined) {
|
||||
downloadToolGroup = ToolGroupManager.createToolGroup(TOOLGROUP_ID);
|
||||
|
||||
// what tools were in the active viewport?
|
||||
// make them all enabled instances so that they can not be interacted
|
||||
// with in the download viewport
|
||||
Object.values(toolGroup._toolInstances).forEach(tool => {
|
||||
downloadToolGroup.addTool(tool.getToolName());
|
||||
});
|
||||
|
||||
// add the viewport to the toolGroup
|
||||
downloadToolGroup.addViewport(downloadViewportId);
|
||||
}
|
||||
|
||||
Object.values(downloadToolGroup._toolInstances).forEach(tool => {
|
||||
const toolName = tool.getToolName();
|
||||
if (toggle) {
|
||||
downloadToolGroup.setToolEnabled(toolName);
|
||||
Object.keys(toolGroup._toolInstances).forEach(toolName => {
|
||||
// make all tools Enabled so that they can not be interacted with
|
||||
// in the download viewport
|
||||
if (toggle && toolName !== 'Crosshairs') {
|
||||
try {
|
||||
toolGroup.setToolEnabled(toolName);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
} else {
|
||||
downloadToolGroup.setToolDisabled(toolName);
|
||||
toolGroup.setToolDisabled(toolName);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -199,10 +240,10 @@ const CornerstoneViewportDownloadForm = ({
|
||||
`div[data-viewport-uid="${VIEWPORT_ID}"]`
|
||||
);
|
||||
|
||||
domtoimage.toPng(divForDownloadViewport).then(dataUrl => {
|
||||
html2canvas(divForDownloadViewport).then(canvas => {
|
||||
const link = document.createElement('a');
|
||||
link.download = file;
|
||||
link.href = dataUrl;
|
||||
link.href = canvas.toDataURL(fileType, 1.0);
|
||||
link.click();
|
||||
});
|
||||
};
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import { Types, getRenderingEngine, utilities } from '@cornerstonejs/core';
|
||||
|
||||
export default function calculateViewportRegistrations(
|
||||
viewports: Types.IViewportId[]
|
||||
) {
|
||||
const viewportPairs = _getViewportPairs(viewports);
|
||||
|
||||
for (const [viewport, nextViewport] of viewportPairs) {
|
||||
// check if they are in the same Frame of Reference
|
||||
const renderingEngine1 = getRenderingEngine(viewport.renderingEngineId);
|
||||
const renderingEngine2 = getRenderingEngine(nextViewport.renderingEngineId);
|
||||
|
||||
const csViewport1 = renderingEngine1.getViewport(viewport.viewportId);
|
||||
const csViewport2 = renderingEngine2.getViewport(nextViewport.viewportId);
|
||||
|
||||
utilities.calculateViewportsSpatialRegistration(csViewport1, csViewport2);
|
||||
}
|
||||
}
|
||||
|
||||
const _getViewportPairs = (viewports: Types.IViewportId[]) => {
|
||||
const viewportPairs = [];
|
||||
|
||||
for (let i = 0; i < viewports.length; i++) {
|
||||
for (let j = i + 1; j < viewports.length; j++) {
|
||||
viewportPairs.push([viewports[i], viewports[j]]);
|
||||
}
|
||||
}
|
||||
|
||||
return viewportPairs;
|
||||
};
|
||||
@ -38,6 +38,10 @@ const getImageInstance = dataset => {
|
||||
return dataset && dataset.images && dataset.images[0];
|
||||
};
|
||||
|
||||
const getNonImageInstance = dataset => {
|
||||
return dataset && dataset.instance;
|
||||
};
|
||||
|
||||
const getImageInstanceId = imageInstance => {
|
||||
return getImageId(imageInstance);
|
||||
};
|
||||
@ -89,19 +93,28 @@ const getImageLoaderType = imageId => {
|
||||
|
||||
class DicomLoaderService {
|
||||
getLocalData(dataset, studies) {
|
||||
if (dataset && dataset.localFile) {
|
||||
// Use referenced imageInstance
|
||||
const imageInstance = getImageInstance(dataset);
|
||||
let imageId = getImageInstanceId(imageInstance);
|
||||
// Use referenced imageInstance
|
||||
const imageInstance = getImageInstance(dataset);
|
||||
const nonImageInstance = getNonImageInstance(dataset);
|
||||
|
||||
// or Try to get it from studies
|
||||
if (someInvalidStrings(imageId)) {
|
||||
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
|
||||
}
|
||||
if (
|
||||
(!imageInstance && !nonImageInstance) ||
|
||||
!nonImageInstance.imageId.startsWith('dicomfile')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!someInvalidStrings(imageId)) {
|
||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||
}
|
||||
const instance = imageInstance || nonImageInstance;
|
||||
|
||||
let imageId = getImageInstanceId(instance);
|
||||
|
||||
// or Try to get it from studies
|
||||
if (someInvalidStrings(imageId)) {
|
||||
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
if (!someInvalidStrings(imageId)) {
|
||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,13 +195,14 @@ class DicomLoaderService {
|
||||
}
|
||||
}
|
||||
|
||||
*getLoaderIterator(dataset, studies) {
|
||||
*getLoaderIterator(dataset, studies, headers) {
|
||||
yield this.getLocalData(dataset, studies);
|
||||
yield this.getDataByImageType(dataset);
|
||||
yield this.getDataByDatasetType(dataset);
|
||||
}
|
||||
|
||||
findDicomDataPromise(dataset, studies) {
|
||||
findDicomDataPromise(dataset, studies, headers) {
|
||||
dataset.authorizationHeaders = headers;
|
||||
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
||||
// it returns first valid retriever method.
|
||||
for (const loader of loaderIterator) {
|
||||
|
||||
@ -8,15 +8,19 @@ const CORONAL = 'coronal';
|
||||
export default function getCornerstoneOrientation(
|
||||
orientation: string
|
||||
): Enums.OrientationAxis {
|
||||
switch (orientation.toLowerCase()) {
|
||||
case AXIAL:
|
||||
return Enums.OrientationAxis.AXIAL;
|
||||
case SAGITTAL:
|
||||
return Enums.OrientationAxis.SAGITTAL;
|
||||
case CORONAL:
|
||||
return Enums.OrientationAxis.CORONAL;
|
||||
default:
|
||||
log.wanr('Choosing acquisition plane orientation');
|
||||
return Enums.OrientationAxis.ACQUISITION;
|
||||
if (orientation) {
|
||||
switch (orientation.toLowerCase()) {
|
||||
case AXIAL:
|
||||
return Enums.OrientationAxis.AXIAL;
|
||||
case SAGITTAL:
|
||||
return Enums.OrientationAxis.SAGITTAL;
|
||||
case CORONAL:
|
||||
return Enums.OrientationAxis.CORONAL;
|
||||
default:
|
||||
log.wanr('Choosing acquisition plane orientation');
|
||||
return Enums.OrientationAxis.ACQUISITION;
|
||||
}
|
||||
}
|
||||
|
||||
return Enums.OrientationAxis.ACQUISITION;
|
||||
}
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Given the ViewportGridService state it will re create the protocol viewport structure
|
||||
* that was used at the hanging protocol creation time. This is used to re create the
|
||||
* viewport structure when the user decides to go back to a previous cached
|
||||
* layout in the viewport grid.
|
||||
*
|
||||
*
|
||||
* viewportGrid's viewports look like
|
||||
*
|
||||
* viewports = [
|
||||
* {
|
||||
* displaySetInstanceUIDs: string[],
|
||||
* displaySetOptions: [],
|
||||
* viewportOptions: {}
|
||||
* height: number,
|
||||
* width: number,
|
||||
* x: number,
|
||||
* y: number
|
||||
* },
|
||||
* ]
|
||||
*
|
||||
* and hanging protocols viewport structure looks like
|
||||
*
|
||||
* viewportStructure: {
|
||||
* layoutType: 'grid',
|
||||
* properties: {
|
||||
* rows: 3,
|
||||
* columns: 4,
|
||||
* layoutOptions: [
|
||||
* {
|
||||
* x: 0,
|
||||
* y: 0,
|
||||
* width: 1 / 4,
|
||||
* height: 1 / 3,
|
||||
* },
|
||||
* {
|
||||
* x: 1 / 4,
|
||||
* y: 0,
|
||||
* width: 1 / 4,
|
||||
* height: 1 / 3,
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
* },
|
||||
*/
|
||||
export default function getProtocolViewportStructureFromGridViewports({
|
||||
numRows,
|
||||
numCols,
|
||||
viewports,
|
||||
}: {
|
||||
numRows: number;
|
||||
numCols: number;
|
||||
viewports: any[];
|
||||
}) {
|
||||
const viewportStructure = {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: numRows,
|
||||
columns: numCols,
|
||||
layoutOptions: [],
|
||||
},
|
||||
};
|
||||
|
||||
viewports.forEach(viewport => {
|
||||
viewportStructure.properties.layoutOptions.push({
|
||||
x: viewport.x,
|
||||
y: viewport.y,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
});
|
||||
|
||||
return viewportStructure;
|
||||
}
|
||||
@ -51,7 +51,7 @@ export default function interleaveCenterLoader({
|
||||
* listen to it and as the other viewports are created we can set the volumes for them
|
||||
* since volumes are already started loading.
|
||||
*/
|
||||
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ export default function interleaveTopToBottom({
|
||||
* listen to it and as the other viewports are created we can set the volumes for them
|
||||
* since volumes are already started loading.
|
||||
*/
|
||||
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
||||
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
import { segmentation } from '@cornerstonejs/tools';
|
||||
|
||||
function removeToolGroupSegmentationRepresentations(toolGroupId) {
|
||||
const representations = segmentation.state.getSegmentationRepresentations(
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
if (!representations || !representations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
representations.forEach(representation => {
|
||||
segmentation.state.removeSegmentationRepresentation(
|
||||
toolGroupId,
|
||||
representation.segmentationRepresentationUID
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export default removeToolGroupSegmentationRepresentations;
|
||||
@ -1,30 +0,0 @@
|
||||
import { Enums as csToolsEnums } from '@cornerstonejs/tools';
|
||||
|
||||
const Labelmap = {
|
||||
toSegmentation: segmentationState => {
|
||||
const {
|
||||
activeSegmentIndex,
|
||||
cachedStats: data,
|
||||
segmentsLocked,
|
||||
representationData,
|
||||
label,
|
||||
segmentationId,
|
||||
text,
|
||||
} = segmentationState;
|
||||
|
||||
const labelmapRepresentationData =
|
||||
representationData[csToolsEnums.SegmentationRepresentations.Labelmap];
|
||||
|
||||
return {
|
||||
id: segmentationId,
|
||||
activeSegmentIndex,
|
||||
segmentsLocked,
|
||||
data,
|
||||
label,
|
||||
volumeId: labelmapRepresentationData.volumeId,
|
||||
displayText: text || [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default Labelmap;
|
||||
@ -1,16 +0,0 @@
|
||||
import Labelmap from './Labelmap';
|
||||
|
||||
const segmentationServiceMappingsFactory = (
|
||||
SegmentationService,
|
||||
DisplaySetService
|
||||
) => {
|
||||
return {
|
||||
Labelmap: {
|
||||
matchingCriteria: {},
|
||||
toSegmentation: csToolsSegmentation =>
|
||||
Labelmap.toSegmentation(csToolsSegmentation, DisplaySetService),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default segmentationServiceMappingsFactory;
|
||||
22
extensions/cornerstone/src/utils/transitions.ts
Normal file
22
extensions/cornerstone/src/utils/transitions.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* It is a bell curved function that uses ease in out quadratic for css
|
||||
* transition timing function for each side of the curve.
|
||||
*
|
||||
* @param {number} x - The current time, in the range [0, 1].
|
||||
* @param {number} baseline - The baseline value to start from and return to.
|
||||
* @returns the value of the transition at time x.
|
||||
*/
|
||||
export function easeInOutBell(x: number, baseline: number): number {
|
||||
const alpha = 1 - baseline;
|
||||
|
||||
// prettier-ignore
|
||||
if (x < 1 / 4) {
|
||||
return 4 * Math.pow(2 * x, 3) * alpha + baseline;
|
||||
} else if (x < 1 / 2) {
|
||||
return (1 - Math.pow(-4 * x + 2, 3) / 2) * alpha + baseline;
|
||||
} else if (x < 3 / 4) {
|
||||
return (1 - Math.pow(4 * x - 2, 3) / 2) * alpha + baseline;
|
||||
} else {
|
||||
return (- 4 * Math.pow(2 * x - 2, 3)) * alpha + baseline;
|
||||
}
|
||||
}
|
||||
@ -43,6 +43,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.3",
|
||||
"@cornerstonejs/calculate-suv": "1.0.2"
|
||||
"@cornerstonejs/calculate-suv": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +166,6 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
|
||||
displaySet.images.forEach(instance => {
|
||||
const NumberOfFrames = instance.NumberOfFrames;
|
||||
|
||||
if (NumberOfFrames > 1) {
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
const imageId = this.getImageIdsForInstance({
|
||||
@ -190,9 +189,14 @@ function createDicomLocalApi(dicomLocalConfig) {
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
if (storedInstance.url) {
|
||||
return storedInstance.url;
|
||||
|
||||
let imageId = storedInstance.url;
|
||||
|
||||
if (frame !== undefined) {
|
||||
imageId += `&frame=${frame}`;
|
||||
}
|
||||
|
||||
return imageId;
|
||||
},
|
||||
deleteStudyMetadataPromise() {
|
||||
console.log('deleteStudyMetadataPromise not implemented');
|
||||
|
||||
@ -423,6 +423,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
// any implementation that stores static copies of the metadata
|
||||
StudyInstanceUID: naturalized.StudyInstanceUID,
|
||||
};
|
||||
// Todo: this needs to be from wado dicom web client
|
||||
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
||||
const ret = (val && val[0]) || undefined;
|
||||
value.Value = ret;
|
||||
@ -440,6 +441,9 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
||||
|
||||
// Adding instanceMetadata to OHIF MetadataProvider
|
||||
naturalizedInstances.forEach((instance, index) => {
|
||||
instance.wadoRoot = dicomWebConfig.wadoRoot;
|
||||
instance.wadoUri = dicomWebConfig.wadoUri;
|
||||
|
||||
const imageId = implementation.getImageIdsForInstance({
|
||||
instance,
|
||||
});
|
||||
|
||||
@ -155,7 +155,6 @@ export default function PanelMeasurementTable({
|
||||
>
|
||||
<MeasurementTable
|
||||
title="Measurements"
|
||||
amount={displayMeasurements.length}
|
||||
data={displayMeasurements}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
|
||||
@ -10,12 +10,17 @@ const { formatDate } = utils;
|
||||
* @param {*} param0
|
||||
*/
|
||||
function PanelStudyBrowser({
|
||||
DisplaySetService,
|
||||
servicesManager,
|
||||
getImageSrc,
|
||||
getStudiesForPatientByStudyInstanceUID,
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
}) {
|
||||
const {
|
||||
HangingProtocolService,
|
||||
DisplaySetService,
|
||||
UINotificationService,
|
||||
} = servicesManager.services;
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
@ -34,10 +39,25 @@ function PanelStudyBrowser({
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
displaySetInstanceUIDs: [displaySetInstanceUID],
|
||||
});
|
||||
let updatedViewports = [];
|
||||
const viewportIndex = activeViewportIndex;
|
||||
try {
|
||||
updatedViewports = HangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
UINotificationService.show({
|
||||
title: 'Thumbnail Double Click',
|
||||
message:
|
||||
'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
};
|
||||
|
||||
// ~~ studyDisplayList
|
||||
@ -227,12 +247,7 @@ function PanelStudyBrowser({
|
||||
}
|
||||
|
||||
PanelStudyBrowser.propTypes = {
|
||||
DisplaySetService: PropTypes.shape({
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
getDisplaySetByUID: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
dataSource: PropTypes.shape({
|
||||
getImageIdsForDisplaySet: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
@ -35,7 +35,7 @@ function WrappedPanelStudyBrowser({
|
||||
|
||||
return (
|
||||
<PanelStudyBrowser
|
||||
DisplaySetService={servicesManager.services.DisplaySetService}
|
||||
servicesManager={servicesManager}
|
||||
dataSource={dataSource}
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByStudyInstanceUID={
|
||||
|
||||
62
extensions/default/src/Toolbar/Toolbar.tsx
Normal file
62
extensions/default/src/Toolbar/Toolbar.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export default function Toolbar({ servicesManager }) {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||
const [buttonState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
|
||||
// Could track buttons and state separately...?
|
||||
useEffect(() => {
|
||||
const { unsubscribe: unsub1 } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||
() => setToolbarButtons(ToolBarService.getButtonSection('primary'))
|
||||
);
|
||||
const { unsubscribe: unsub2 } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => setButtonState({ ...ToolBarService.state })
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsub1();
|
||||
unsub2();
|
||||
};
|
||||
}, [ToolBarService]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
// TODO: ...
|
||||
|
||||
// isActive if:
|
||||
// - id is primary?
|
||||
// - id is in list of "toggled on"?
|
||||
let isActive;
|
||||
if (componentProps.type === 'toggle') {
|
||||
isActive = buttonState.toggles[id];
|
||||
}
|
||||
// Also need... to filter list for splitButton, and set primary based on most recently clicked
|
||||
// Also need to kill the radioGroup button's magic logic
|
||||
// Everything should be reactive off these props, so commands can inform ToolbarService
|
||||
|
||||
// These can... Trigger toolbar events based on updates?
|
||||
// Then sync using useEffect, or simply modify the state here?
|
||||
return (
|
||||
<Component
|
||||
key={id}
|
||||
id={id}
|
||||
{...componentProps}
|
||||
bState={buttonState}
|
||||
isActive={isActive}
|
||||
onInteraction={args => ToolBarService.recordInteraction(args)}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,20 +1,43 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
LayoutSelector as OHIFLayoutSelector,
|
||||
ToolbarButton,
|
||||
useViewportGrid,
|
||||
} from '@ohif/ui';
|
||||
|
||||
function LayoutSelector({rows, columns}) {
|
||||
function LayoutSelector({ rows, columns, servicesManager }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [disableSelector, setDisableSelector] = useState(false);
|
||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||
|
||||
const { HangingProtocolService } = servicesManager.services;
|
||||
|
||||
const closeOnOutsideClick = () => {
|
||||
if (isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = HangingProtocolService.subscribe(
|
||||
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||
evt => {
|
||||
const { protocol } = evt;
|
||||
|
||||
if (protocol.id === 'mpr') {
|
||||
setDisableSelector(true);
|
||||
} else {
|
||||
setDisableSelector(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [HangingProtocolService]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('click', closeOnOutsideClick);
|
||||
return () => {
|
||||
@ -44,15 +67,27 @@ function LayoutSelector({rows, columns}) {
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
onSelection={({ numRows, numCols }) => {
|
||||
viewportGridService.setLayout({ numCols, numRows });
|
||||
viewportGridService.setLayout({ numRows, numCols });
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
isActive={isOpen}
|
||||
isActive={disableSelector ? false : isOpen}
|
||||
type="toggle"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
LayoutSelector.propTypes = {
|
||||
rows: PropTypes.number,
|
||||
columns: PropTypes.number,
|
||||
onLayoutChange: PropTypes.func,
|
||||
};
|
||||
|
||||
LayoutSelector.defaultProps = {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
onLayoutChange: () => {},
|
||||
};
|
||||
|
||||
export default LayoutSelector;
|
||||
|
||||
@ -9,74 +9,16 @@ import {
|
||||
AboutModal,
|
||||
Header,
|
||||
useModal,
|
||||
LoadingIndicatorProgress,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
|
||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
||||
|
||||
function Toolbar({ servicesManager }) {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||
const [buttonState, setButtonState] = useState({
|
||||
primaryToolId: '',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
});
|
||||
|
||||
// Could track buttons and state separately...?
|
||||
useEffect(() => {
|
||||
const { unsubscribe: unsub1 } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_MODIFIED,
|
||||
() => setToolbarButtons(ToolBarService.getButtonSection('primary'))
|
||||
);
|
||||
const { unsubscribe: unsub2 } = ToolBarService.subscribe(
|
||||
ToolBarService.EVENTS.TOOL_BAR_STATE_MODIFIED,
|
||||
() => setButtonState({ ...ToolBarService.state })
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsub1();
|
||||
unsub2();
|
||||
};
|
||||
}, [ToolBarService]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{toolbarButtons.map((toolDef, index) => {
|
||||
const { id, Component, componentProps } = toolDef;
|
||||
// TODO: ...
|
||||
|
||||
// isActive if:
|
||||
// - id is primary?
|
||||
// - id is in list of "toggled on"?
|
||||
let isActive;
|
||||
if (componentProps.type === 'toggle') {
|
||||
isActive = buttonState.toggles[id];
|
||||
}
|
||||
// Also need... to filter list for splitButton, and set primary based on most recently clicked
|
||||
// Also need to kill the radioGroup button's magic logic
|
||||
// Everything should be reactive off these props, so commands can inform ToolbarService
|
||||
|
||||
// These can... Trigger toolbar events based on updates?
|
||||
// Then sync using useEffect, or simply modify the state here?
|
||||
return (
|
||||
<Component
|
||||
key={id}
|
||||
id={id}
|
||||
{...componentProps}
|
||||
bState={buttonState}
|
||||
isActive={isActive}
|
||||
onInteraction={args => ToolBarService.recordInteraction(args)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewerLayout({
|
||||
// From Extension Module Params
|
||||
extensionManager,
|
||||
@ -84,12 +26,12 @@ function ViewerLayout({
|
||||
hotkeysManager,
|
||||
commandsManager,
|
||||
// From Modes
|
||||
leftPanels,
|
||||
rightPanels,
|
||||
leftPanelDefaultClosed,
|
||||
rightPanelDefaultClosed,
|
||||
viewports,
|
||||
ViewportGridComp,
|
||||
leftPanels = [],
|
||||
rightPanels = [],
|
||||
leftPanelDefaultClosed = false,
|
||||
rightPanelDefaultClosed = false,
|
||||
}) {
|
||||
const [appConfig] = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
@ -101,6 +43,12 @@ function ViewerLayout({
|
||||
const { t } = useTranslation();
|
||||
const { show, hide } = useModal();
|
||||
|
||||
const [showLoadingIndicator, setShowLoadingIndicator] = useState(
|
||||
appConfig.showLoadingIndicator
|
||||
);
|
||||
|
||||
const { HangingProtocolService } = servicesManager.services;
|
||||
|
||||
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
||||
const versionNumber = process.env.VERSION_NUMBER;
|
||||
const buildNumber = process.env.BUILD_NUM;
|
||||
@ -153,8 +101,10 @@ function ViewerLayout({
|
||||
title: t('Header:Logout'),
|
||||
icon: 'power-off',
|
||||
onClick: async () => {
|
||||
navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`);
|
||||
}
|
||||
navigate(
|
||||
`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -186,6 +136,25 @@ function ViewerLayout({
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = HangingProtocolService.subscribe(
|
||||
HangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
|
||||
|
||||
// Todo: right now to set the loading indicator to false, we need to wait for the
|
||||
// HangingProtocolService to finish applying the viewport matching to each viewport,
|
||||
// however, this might not be the only approach to set the loading indicator to false. we need to explore this further.
|
||||
({ progress }) => {
|
||||
if (progress === 100) {
|
||||
setShowLoadingIndicator(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [HangingProtocolService]);
|
||||
|
||||
const getViewportComponentData = viewportComponent => {
|
||||
const entry = extensionManager.getModuleEntry(viewportComponent.namespace);
|
||||
|
||||
@ -213,44 +182,45 @@ function ViewerLayout({
|
||||
</ErrorBoundary>
|
||||
</Header>
|
||||
<div
|
||||
className="bg-black flex flex-row items-stretch w-full overflow-hidden flex-nowrap"
|
||||
className="bg-black flex flex-row items-stretch w-full overflow-hidden flex-nowrap relative"
|
||||
style={{ height: 'calc(100vh - 52px' }}
|
||||
>
|
||||
{/* LEFT SIDEPANELS */}
|
||||
{leftPanelComponents.length ? (
|
||||
<ErrorBoundary context="Left Panel">
|
||||
<SidePanel
|
||||
side="left"
|
||||
defaultComponentOpen={
|
||||
leftPanelDefaultClosed ? null : leftPanelComponents[0].name
|
||||
}
|
||||
childComponents={leftPanelComponents}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
{/* TOOLBAR + GRID */}
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<div className="flex items-center justify-center flex-1 h-full overflow-hidden bg-black">
|
||||
<ErrorBoundary context="Grid">
|
||||
<ViewportGridComp
|
||||
servicesManager={servicesManager}
|
||||
viewportComponents={viewportComponents}
|
||||
commandsManager={commandsManager}
|
||||
<React.Fragment>
|
||||
{showLoadingIndicator && (
|
||||
<LoadingIndicatorProgress className="h-full w-full bg-black" />
|
||||
)}
|
||||
{/* LEFT SIDEPANELS */}
|
||||
{leftPanelComponents.length ? (
|
||||
<ErrorBoundary context="Left Panel">
|
||||
<SidePanel
|
||||
side="left"
|
||||
activeTabIndex={leftPanelDefaultClosed ? null : 0}
|
||||
tabs={leftPanelComponents}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
{/* TOOLBAR + GRID */}
|
||||
<div className="flex flex-col flex-1 h-full">
|
||||
<div className="flex items-center justify-center flex-1 h-full overflow-hidden bg-black relative">
|
||||
<ErrorBoundary context="Grid">
|
||||
<ViewportGridComp
|
||||
servicesManager={servicesManager}
|
||||
viewportComponents={viewportComponents}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{rightPanelComponents.length ? (
|
||||
<ErrorBoundary context="Right Panel">
|
||||
<SidePanel
|
||||
side="right"
|
||||
defaultComponentOpen={
|
||||
rightPanelDefaultClosed ? null : rightPanelComponents[0].name
|
||||
}
|
||||
childComponents={rightPanelComponents}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
{rightPanelComponents.length ? (
|
||||
<ErrorBoundary context="Right Panel">
|
||||
<SidePanel
|
||||
side="right"
|
||||
activeTabIndex={rightPanelDefaultClosed ? null : 0}
|
||||
tabs={rightPanelComponents}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -271,11 +241,4 @@ ViewerLayout.propTypes = {
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
};
|
||||
|
||||
ViewerLayout.defaultProps = {
|
||||
leftPanels: [],
|
||||
rightPanels: [],
|
||||
leftPanelDefaultClosed: false,
|
||||
rightPanelDefaultClosed: false,
|
||||
};
|
||||
|
||||
export default ViewerLayout;
|
||||
|
||||
@ -34,9 +34,10 @@ function getPanelModule({
|
||||
},
|
||||
{
|
||||
name: 'measure',
|
||||
iconName: 'list-bullets',
|
||||
iconName: 'tab-linear',
|
||||
iconLabel: 'Measure',
|
||||
label: 'Measurements',
|
||||
secondaryLabel: 'Measurements',
|
||||
component: wrappedMeasurementPanel,
|
||||
},
|
||||
];
|
||||
|
||||
@ -25,6 +25,7 @@ const makeDisplaySet = instances => {
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
SeriesNumber: instance.SeriesNumber || 0,
|
||||
FrameRate: instance.FrameTime,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
SeriesDescription: instance.SeriesDescription || '',
|
||||
Modality: instance.Modality,
|
||||
isMultiFrame: isMultiFrame(instance),
|
||||
|
||||
@ -26,6 +26,7 @@ const _getDisplaySetsFromSeries = (
|
||||
SeriesDate,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassUID,
|
||||
} = instance;
|
||||
const pdfUrl = dataSource.retrieve.directURL({
|
||||
instance,
|
||||
@ -45,6 +46,7 @@ const _getDisplaySetsFromSeries = (
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
referencedImages: null,
|
||||
measurements: null,
|
||||
pdfUrl,
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^3.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"@cornerstonejs/core": "^0.16.8",
|
||||
"@cornerstonejs/tools": "^0.24.1",
|
||||
"@cornerstonejs/core": "^0.21.0",
|
||||
"@cornerstonejs/tools": "^0.29.2",
|
||||
"@ohif/extension-cornerstone-dicom-sr": "^3.0.0",
|
||||
"dcmjs": "^0.28.3",
|
||||
"prop-types": "^15.6.2",
|
||||
|
||||
@ -26,7 +26,7 @@ function getPanelModule({
|
||||
},
|
||||
{
|
||||
name: 'trackedMeasurements',
|
||||
iconName: 'list-bullets',
|
||||
iconName: 'tab-linear',
|
||||
iconLabel: 'Measure',
|
||||
label: 'Measurements',
|
||||
component: PanelMeasurementTableTracking.bind(null, {
|
||||
|
||||
@ -251,7 +251,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
)}
|
||||
<MeasurementTable
|
||||
title="Measurements"
|
||||
amount={displayMeasurementsWithoutFindings.length}
|
||||
data={displayMeasurementsWithoutFindings}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
@ -259,7 +258,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
{additionalFindings.length !== 0 && (
|
||||
<MeasurementTable
|
||||
title="Additional Findings"
|
||||
amount={additionalFindings.length}
|
||||
data={additionalFindings}
|
||||
onClick={jumpToImage}
|
||||
onEdit={onMeasurementItemEditHandler}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { utils } from '@ohif/core';
|
||||
import { HangingProtocolService, utils } from '@ohif/core';
|
||||
import {
|
||||
StudyBrowser,
|
||||
useImageViewer,
|
||||
@ -16,15 +16,20 @@ const { formatDate } = utils;
|
||||
* @param {*} param0
|
||||
*/
|
||||
function PanelStudyBrowserTracking({
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
UIDialogService,
|
||||
UINotificationService,
|
||||
servicesManager,
|
||||
getImageSrc,
|
||||
getStudiesForPatientByStudyInstanceUID,
|
||||
requestDisplaySetCreationForStudy,
|
||||
dataSource,
|
||||
}) {
|
||||
const {
|
||||
MeasurementService,
|
||||
DisplaySetService,
|
||||
UIDialogService,
|
||||
HangingProtocolService,
|
||||
UINotificationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// Normally you nest the components so the tree isn't so deep, and the data
|
||||
// doesn't have to have such an intense shape. This works well enough for now.
|
||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||
@ -47,10 +52,25 @@ function PanelStudyBrowserTracking({
|
||||
const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null);
|
||||
|
||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
displaySetInstanceUIDs: [displaySetInstanceUID],
|
||||
});
|
||||
let updatedViewports = [];
|
||||
const viewportIndex = activeViewportIndex;
|
||||
try {
|
||||
updatedViewports = HangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
UINotificationService.show({
|
||||
title: 'Thumbnail Double Click',
|
||||
message:
|
||||
'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
};
|
||||
|
||||
const activeViewportDisplaySetInstanceUIDs =
|
||||
@ -353,16 +373,7 @@ function PanelStudyBrowserTracking({
|
||||
}
|
||||
|
||||
PanelStudyBrowserTracking.propTypes = {
|
||||
MeasurementService: PropTypes.shape({
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
DisplaySetService: PropTypes.shape({
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
getDisplaySetByUID: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
dataSource: PropTypes.shape({
|
||||
getImageIdsForDisplaySet: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
@ -542,8 +553,6 @@ function _getComponentType(Modality) {
|
||||
return 'thumbnailTracked';
|
||||
}
|
||||
|
||||
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string[]} primaryStudyInstanceUIDs
|
||||
|
||||
@ -45,10 +45,7 @@ function WrappedPanelStudyBrowserTracking({
|
||||
|
||||
return (
|
||||
<PanelStudyBrowserTracking
|
||||
MeasurementService={servicesManager.services.MeasurementService}
|
||||
DisplaySetService={servicesManager.services.DisplaySetService}
|
||||
UIDialogService={servicesManager.services.UIDialogService}
|
||||
UINotificationService={servicesManager.services.UINotificationService}
|
||||
servicesManager={servicesManager}
|
||||
dataSource={dataSource}
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByStudyInstanceUID={
|
||||
|
||||
@ -8,8 +8,12 @@ import {
|
||||
useCine,
|
||||
useViewportGrid,
|
||||
useViewportDialog,
|
||||
Tooltip,
|
||||
Icon,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { eventTarget, Enums } from '@cornerstonejs/core';
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import { useTrackedMeasurements } from './../getContextModule';
|
||||
@ -27,6 +31,8 @@ function TrackedCornerstoneViewport(props) {
|
||||
commandsManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('TrackedViewport');
|
||||
|
||||
const {
|
||||
MeasurementService,
|
||||
CornerstoneViewportService,
|
||||
@ -137,9 +143,11 @@ function TrackedCornerstoneViewport(props) {
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
cineService.stopClip(element);
|
||||
if (element && cines?.[viewportIndex]?.isPlaying) {
|
||||
cineService.stopClip(element);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportIndex, cineService, element]);
|
||||
}, [cines, viewportIndex, cineService, element, cineHandler]);
|
||||
|
||||
if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) {
|
||||
setIsTracked(!isTracked);
|
||||
@ -193,16 +201,14 @@ function TrackedCornerstoneViewport(props) {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onSeriesChange={direction => switchMeasurement(direction)}
|
||||
useAltStyling={isTracked}
|
||||
onArrowsClick={direction => switchMeasurement(direction)}
|
||||
getStatusComponent={() => _getStatusComponent(isTracked)}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
isTracked,
|
||||
isLocked: false,
|
||||
isRehydratable: false,
|
||||
studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok?
|
||||
currentSeries: SeriesNumber, // TODO - switch entire currentSeries to be UID based or actual position based
|
||||
seriesDescription: SeriesDescription,
|
||||
modality: Modality,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
@ -322,4 +328,43 @@ function _getNextMeasurementUID(
|
||||
return newTrackedMeasurementId;
|
||||
}
|
||||
|
||||
function _getStatusComponent(isTracked) {
|
||||
const trackedIcon = isTracked ? 'tracked' : 'dotted-circle';
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Tooltip
|
||||
position="bottom-left"
|
||||
content={
|
||||
<div className="flex py-2">
|
||||
<div className="flex pt-1">
|
||||
<Icon name="info-link" className="w-4 text-primary-main" />
|
||||
</div>
|
||||
<div className="flex ml-4">
|
||||
<span className="text-base text-common-light">
|
||||
{isTracked ? (
|
||||
<>
|
||||
Series is
|
||||
<span className="font-bold text-white"> tracked</span> and
|
||||
can be viewed <br /> in the measurement panel
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Measurements for
|
||||
<span className="font-bold text-white"> untracked </span>
|
||||
series <br /> will not be shown in the <br /> measurements
|
||||
panel
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name={trackedIcon} className="w-6 text-primary-light" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrackedCornerstoneViewport;
|
||||
|
||||
@ -143,7 +143,6 @@ export default function PanelPetSUV({ servicesManager, commandsManager }) {
|
||||
ptDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
||||
{
|
||||
|
||||
@ -94,7 +94,7 @@ export default function PanelRoiThresholdSegmentation({
|
||||
selectedSegmentationId
|
||||
);
|
||||
|
||||
const data = {
|
||||
const cachedStats = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
@ -102,11 +102,10 @@ export default function PanelRoiThresholdSegmentation({
|
||||
|
||||
const notYetUpdatedAtSource = true;
|
||||
SegmentationService.addOrUpdateSegmentation(
|
||||
selectedSegmentationId,
|
||||
{
|
||||
...segmentation,
|
||||
data: Object.assign(segmentation.data, data),
|
||||
text: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
|
||||
...Object.assign(segmentation.cachedStats, cachedStats),
|
||||
displayText: [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`],
|
||||
},
|
||||
notYetUpdatedAtSource
|
||||
);
|
||||
@ -160,23 +159,6 @@ export default function PanelRoiThresholdSegmentation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Toggle visibility of the segmentation
|
||||
*/
|
||||
useEffect(() => {
|
||||
const subscription = SegmentationService.subscribe(
|
||||
SegmentationService.EVENTS.SEGMENTATION_VISIBILITY_CHANGED,
|
||||
({ segmentation }) => {
|
||||
runCommand('toggleSegmentationVisibility', {
|
||||
segmentationId: segmentation.id,
|
||||
});
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [SegmentationService]);
|
||||
|
||||
/**
|
||||
* Whenever the segmentations change, update the TMTV calculations
|
||||
*/
|
||||
@ -189,7 +171,7 @@ export default function PanelRoiThresholdSegmentation({
|
||||
}, [segmentations, selectedSegmentationId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="flex flex-col">
|
||||
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
|
||||
<div className="flex mx-4 my-4 mb-4 space-x-4">
|
||||
<Button
|
||||
@ -203,17 +185,10 @@ export default function PanelRoiThresholdSegmentation({
|
||||
});
|
||||
});
|
||||
}}
|
||||
className={classnames(
|
||||
'text-xs text-white border-b border-transparent'
|
||||
)}
|
||||
>
|
||||
{labelmapLoading ? 'loading ...' : 'New Label'}
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={handleROIThresholding}
|
||||
className="text-xs text-white border-b border-transparent "
|
||||
>
|
||||
<Button color="primary" onClick={handleROIThresholding}>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
@ -239,7 +214,6 @@ export default function PanelRoiThresholdSegmentation({
|
||||
{segmentations?.length ? (
|
||||
<SegmentationTable
|
||||
title={t('Segmentations')}
|
||||
amount={segmentations.length}
|
||||
segmentations={segmentations}
|
||||
activeSegmentationId={selectedSegmentationId}
|
||||
onClick={id => {
|
||||
@ -249,10 +223,12 @@ export default function PanelRoiThresholdSegmentation({
|
||||
setSelectedSegmentationId(id);
|
||||
}}
|
||||
onToggleVisibility={id => {
|
||||
SegmentationService.toggleSegmentationsVisibility([id]);
|
||||
SegmentationService.toggleSegmentationVisibility(id);
|
||||
}}
|
||||
onToggleVisibilityAll={ids => {
|
||||
SegmentationService.toggleSegmentationsVisibility(ids);
|
||||
ids.map(id => {
|
||||
SegmentationService.toggleSegmentationVisibility(id);
|
||||
});
|
||||
}}
|
||||
onDelete={id => {
|
||||
SegmentationService.remove(id);
|
||||
@ -282,11 +258,11 @@ export default function PanelRoiThresholdSegmentation({
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="opacity-50 hover:opacity-80 flex items-center justify-center text-blue-400 mb-4 cursor-pointer"
|
||||
className="opacity-50 hover:opacity-80 flex items-center justify-center text-blue-400 mt-auto cursor-pointer mb-4"
|
||||
onClick={() => {
|
||||
// navigate to a url in a new tab
|
||||
window.open(
|
||||
'https://github.com/OHIF/Viewers/blob/feat/segmentation-service/modes/tmtv/README.md',
|
||||
'https://github.com/OHIF/Viewers/blob/v3-stable/modes/tmtv/README.md',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
@ -312,10 +288,9 @@ PanelRoiThresholdSegmentation.propTypes = {
|
||||
SegmentationService: PropTypes.shape({
|
||||
getSegmentation: PropTypes.func.isRequired,
|
||||
getSegmentations: PropTypes.func.isRequired,
|
||||
toggleSegmentationsVisibility: PropTypes.func.isRequired,
|
||||
toggleSegmentationVisibility: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
|
||||
@ -20,7 +20,7 @@ function ROIThresholdConfiguration({ config, dispatch, runCommand }) {
|
||||
<Select
|
||||
label={t('Strategy')}
|
||||
closeMenuOnSelect={true}
|
||||
className="mr-2 bg-black border-primary-main "
|
||||
className="mr-2 bg-black border-primary-main text-white "
|
||||
options={options}
|
||||
placeholder={
|
||||
options.find(option => option.value === config.strategy)
|
||||
|
||||
@ -10,7 +10,6 @@ function segmentationItemEditHandler({ id, servicesManager }) {
|
||||
switch (action.id) {
|
||||
case 'save': {
|
||||
SegmentationService.addOrUpdateSegmentation(
|
||||
id,
|
||||
{
|
||||
...segmentation,
|
||||
...value,
|
||||
|
||||
@ -12,6 +12,7 @@ import dicomRTAnnotationExport from './utils/dicomRTAnnotationExport/RTStructure
|
||||
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
const RECTANGLE_ROI_THRESHOLD_MANUAL = 'RectangleROIStartEndThreshold';
|
||||
const LABELMAP = csTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const commandsModule = ({
|
||||
servicesManager,
|
||||
@ -25,6 +26,7 @@ const commandsModule = ({
|
||||
HangingProtocolService,
|
||||
ToolGroupService,
|
||||
CornerstoneViewportService,
|
||||
SegmentationService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
@ -43,7 +45,8 @@ const commandsModule = ({
|
||||
function _getMatchedViewportsToolGroupIds() {
|
||||
const { viewportMatchDetails } = HangingProtocolService.getMatchDetails();
|
||||
const toolGroupIds = [];
|
||||
viewportMatchDetails.forEach(({ viewportOptions }) => {
|
||||
viewportMatchDetails.forEach((value, key) => {
|
||||
const { viewportOptions } = value;
|
||||
const { toolGroupId } = viewportOptions;
|
||||
if (toolGroupIds.indexOf(toolGroupId) === -1) {
|
||||
toolGroupIds.push(toolGroupId);
|
||||
@ -61,8 +64,8 @@ const commandsModule = ({
|
||||
// corrected PT vs the non-attenuation correct PT)
|
||||
|
||||
let ptDisplaySet = null;
|
||||
for (const matched of viewportMatchDetails) {
|
||||
const { displaySetsInfo } = matched;
|
||||
for (const [viewportIndex, viewportDetails] of viewportMatchDetails) {
|
||||
const { displaySetsInfo } = viewportDetails;
|
||||
const displaySets = displaySetsInfo.map(({ displaySetInstanceUID }) =>
|
||||
DisplaySetService.getDisplaySetByUID(displaySetInstanceUID)
|
||||
);
|
||||
@ -128,22 +131,27 @@ const commandsModule = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentationId = await commandsManager.runCommand(
|
||||
'createSegmentationForDisplaySet',
|
||||
{
|
||||
displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
|
||||
}
|
||||
const segmentationId = await SegmentationService.createSegmentationForDisplaySet(
|
||||
ptDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
||||
|
||||
const representationType =
|
||||
csTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
const representationType = LABELMAP;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
await commandsManager.runCommand(
|
||||
'addSegmentationRepresentationToToolGroup',
|
||||
{ segmentationId, toolGroupId: toolGroupId, representationType }
|
||||
const hydrateSegmentation = true;
|
||||
await SegmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
SegmentationService.setActiveSegmentationForToolGroup(
|
||||
segmentationId,
|
||||
toolGroupId
|
||||
);
|
||||
}
|
||||
|
||||
@ -153,24 +161,10 @@ const commandsModule = ({
|
||||
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
|
||||
SegmentationService.setActiveSegmentationForToolGroup(
|
||||
segmentationId,
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
if (segmentationRepresentations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo: this finds the first segmentation representation that matches the segmentationId
|
||||
// If there are two labelmap representations from the same segmentation, this will not work
|
||||
const representation = segmentationRepresentations.find(
|
||||
representation => representation.segmentationId === segmentationId
|
||||
);
|
||||
|
||||
csTools.segmentation.activeSegmentation.setActiveSegmentationRepresentation(
|
||||
toolGroupId,
|
||||
representation.segmentationRepresentationUID
|
||||
);
|
||||
});
|
||||
},
|
||||
thresholdSegmentationByRectangleROITool: ({ segmentationId, config }) => {
|
||||
@ -179,9 +173,7 @@ const commandsModule = ({
|
||||
);
|
||||
|
||||
const { representationData } = segmentation;
|
||||
const { volumeId: segVolumeId } = representationData[
|
||||
csTools.Enums.SegmentationRepresentations.Labelmap
|
||||
];
|
||||
const { volumeId: segVolumeId } = representationData[LABELMAP];
|
||||
|
||||
const { referencedVolumeId } = cs.cache.getVolume(segVolumeId);
|
||||
|
||||
@ -228,36 +220,6 @@ const commandsModule = ({
|
||||
configToUse
|
||||
);
|
||||
},
|
||||
toggleSegmentationVisibility: ({ segmentationId }) => {
|
||||
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationRepresentations(
|
||||
toolGroupId
|
||||
);
|
||||
|
||||
if (segmentationRepresentations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo: this finds the first segmentation representation that matches the segmentationId
|
||||
// If there are two labelmap representations from the same segmentation, this will not work
|
||||
const representation = segmentationRepresentations.find(
|
||||
representation => representation.segmentationId === segmentationId
|
||||
);
|
||||
|
||||
const visibility = csTools.segmentation.config.visibility.getSegmentationVisibility(
|
||||
toolGroupId,
|
||||
representation.segmentationRepresentationUID
|
||||
);
|
||||
|
||||
csTools.segmentation.config.visibility.setSegmentationVisibility(
|
||||
toolGroupId,
|
||||
representation.segmentationRepresentationUID,
|
||||
!visibility
|
||||
);
|
||||
});
|
||||
},
|
||||
calculateSuvPeak: ({ labelmap }) => {
|
||||
const { referencedVolumeId } = labelmap;
|
||||
|
||||
@ -326,9 +288,9 @@ const commandsModule = ({
|
||||
};
|
||||
},
|
||||
calculateTMTV: ({ segmentations }) => {
|
||||
const labelmaps = commandsManager.runCommand('getLabelmapVolumes', {
|
||||
segmentations,
|
||||
});
|
||||
const labelmaps = segmentations.map(s =>
|
||||
SegmentationService.getLabelmapVolume(s.id)
|
||||
);
|
||||
|
||||
if (!labelmaps.length) {
|
||||
return;
|
||||
@ -351,9 +313,9 @@ const commandsModule = ({
|
||||
createAndDownloadTMTVReport(segReport, additionalReportRows);
|
||||
},
|
||||
getTotalLesionGlycolysis: ({ segmentations }) => {
|
||||
const labelmapVolumes = commandsManager.runCommand('getLabelmapVolumes', {
|
||||
segmentations,
|
||||
});
|
||||
const labelmapVolumes = segmentations.map(s =>
|
||||
SegmentationService.getLabelmapVolume(s.id)
|
||||
);
|
||||
|
||||
let mergedLabelmap;
|
||||
// merge labelmap will through an error if labels maps are not the same size
|
||||
@ -408,7 +370,6 @@ const commandsModule = ({
|
||||
1e-3
|
||||
);
|
||||
},
|
||||
|
||||
setStartSliceForROIThresholdTool: () => {
|
||||
const { viewport } = _getActiveViewportsEnabledElement();
|
||||
const { focalPoint, viewPlaneNormal } = viewport.getCamera();
|
||||
@ -493,7 +454,7 @@ const commandsModule = ({
|
||||
let report = {};
|
||||
|
||||
for (const segmentation of segmentations) {
|
||||
const { id, label, data } = segmentation;
|
||||
const { id, label, cachedStats: data } = segmentation;
|
||||
|
||||
const segReport = { id, label };
|
||||
|
||||
@ -513,7 +474,7 @@ const commandsModule = ({
|
||||
}
|
||||
});
|
||||
|
||||
const labelmapVolume = cornerstone.cache.getVolume(id);
|
||||
const labelmapVolume = SegmentationService.getLabelmapVolume(id);
|
||||
|
||||
if (!labelmapVolume) {
|
||||
report[id] = segReport;
|
||||
@ -523,7 +484,7 @@ const commandsModule = ({
|
||||
const referencedVolumeId = labelmapVolume.referencedVolumeId;
|
||||
segReport.referencedVolumeId = referencedVolumeId;
|
||||
|
||||
const referencedVolume = cornerstone.cache.getVolume(
|
||||
const referencedVolume = SegmentationService.getLabelmapVolume(
|
||||
referencedVolumeId
|
||||
);
|
||||
|
||||
@ -637,11 +598,6 @@ const commandsModule = ({
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleSegmentationVisibility: {
|
||||
commandFn: actions.toggleSegmentationVisibility,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getTotalLesionGlycolysis: {
|
||||
commandFn: actions.getTotalLesionGlycolysis,
|
||||
storeContexts: [],
|
||||
|
||||
@ -7,12 +7,6 @@ const ptCT = {
|
||||
modifiedDate: '2022-10-04T19:22:08.894Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
toolGroupIds: [
|
||||
'ctToolGroup',
|
||||
'ptToolGroup',
|
||||
'fusionToolGroup',
|
||||
'mipToolGroup',
|
||||
],
|
||||
imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
@ -47,6 +41,16 @@ const ptCT = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
@ -70,6 +74,16 @@ const ptCT = {
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
|
||||
@ -34,14 +34,14 @@ function getPanelModule({
|
||||
return [
|
||||
{
|
||||
name: 'petSUV',
|
||||
iconName: 'edit-patient',
|
||||
iconName: 'tab-patient-info',
|
||||
iconLabel: 'PET SUV',
|
||||
label: 'PET SUV',
|
||||
component: wrappedPanelPetSuv,
|
||||
},
|
||||
{
|
||||
name: 'ROIThresholdSeg',
|
||||
iconName: 'tool-create-threshold',
|
||||
iconName: 'tab-roi-threshold',
|
||||
iconLabel: 'ROI Threshold',
|
||||
label: 'ROI Threshold',
|
||||
component: wrappedROIThresholdSeg,
|
||||
|
||||
@ -10,6 +10,7 @@ const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG'];
|
||||
const ohif = {
|
||||
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
|
||||
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
|
||||
};
|
||||
|
||||
const tracked = {
|
||||
@ -37,12 +38,20 @@ const dicompdf = {
|
||||
viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf',
|
||||
};
|
||||
|
||||
const dicomSeg = {
|
||||
sopClassHandler:
|
||||
'@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
|
||||
panel: '@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation',
|
||||
};
|
||||
|
||||
const extensionDependencies = {
|
||||
// Can derive the versions at least process.env.from npm_package_version
|
||||
'@ohif/extension-default': '^3.0.0',
|
||||
'@ohif/extension-cornerstone': '^3.0.0',
|
||||
'@ohif/extension-measurement-tracking': '^3.0.0',
|
||||
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
|
||||
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
|
||||
'@ohif/extension-dicom-pdf': '^3.0.1',
|
||||
'@ohif/extension-dicom-video': '^3.0.1',
|
||||
};
|
||||
@ -102,7 +111,8 @@ function modeFactory() {
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
// 'MPR',
|
||||
'MPR',
|
||||
'Crosshairs',
|
||||
'MoreTools',
|
||||
]);
|
||||
},
|
||||
@ -112,12 +122,18 @@ function modeFactory() {
|
||||
SyncGroupService,
|
||||
MeasurementService,
|
||||
ToolBarService,
|
||||
SegmentationService,
|
||||
CornerstoneViewportService,
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
ToolBarService.reset();
|
||||
MeasurementService.clearMeasurements();
|
||||
ToolGroupService.destroy();
|
||||
SyncGroupService.destroy();
|
||||
SegmentationService.destroy();
|
||||
CornerstoneViewportService.destroy();
|
||||
HangingProtocolService.reset();
|
||||
},
|
||||
validationTags: {
|
||||
study: [],
|
||||
@ -143,8 +159,8 @@ function modeFactory() {
|
||||
id: ohif.layout,
|
||||
props: {
|
||||
leftPanels: [tracked.thumbnailList],
|
||||
// TODO: Should be optional, or required to pass empty array for slots?
|
||||
rightPanels: [tracked.measurements],
|
||||
rightPanels: [dicomSeg.panel, tracked.measurements],
|
||||
// rightPanelDefaultClosed: true, // optional prop to start with collapse panels
|
||||
viewports: [
|
||||
{
|
||||
namespace: tracked.viewport,
|
||||
@ -162,6 +178,10 @@ function modeFactory() {
|
||||
namespace: dicompdf.viewport,
|
||||
displaySetsToDisplay: [dicompdf.sopClassHandler],
|
||||
},
|
||||
{
|
||||
namespace: dicomSeg.viewport,
|
||||
displaySetsToDisplay: [dicomSeg.sopClassHandler],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -177,6 +197,7 @@ function modeFactory() {
|
||||
// come first to remove video transfer syntax before ohif uses images
|
||||
sopClassHandlers: [
|
||||
dicomvideo.sopClassHandler,
|
||||
dicomSeg.sopClassHandler,
|
||||
ohif.sopClassHandler,
|
||||
dicompdf.sopClassHandler,
|
||||
dicomsr.sopClassHandler,
|
||||
|
||||
@ -36,9 +36,11 @@ function initDefaultToolGroup(
|
||||
{ toolName: toolNames.StackScroll },
|
||||
{ toolName: toolNames.Angle },
|
||||
{ toolName: toolNames.Magnify },
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
],
|
||||
// enabled
|
||||
// disabled
|
||||
disabled: [{ toolName: toolNames.ReferenceLines }],
|
||||
};
|
||||
|
||||
const toolsConfig = {
|
||||
@ -139,6 +141,73 @@ function initSRToolGroup(extensionManager, ToolGroupService, commandsManager) {
|
||||
ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig);
|
||||
}
|
||||
|
||||
function initMPRToolGroup(extensionManager, ToolGroupService, commandsManager) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.tools'
|
||||
);
|
||||
|
||||
const { toolNames, Enums } = utilityModule.exports;
|
||||
|
||||
const tools = {
|
||||
active: [
|
||||
{
|
||||
toolName: toolNames.WindowLevel,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Pan,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Auxiliary }],
|
||||
},
|
||||
{
|
||||
toolName: toolNames.Zoom,
|
||||
bindings: [{ mouseButton: Enums.MouseBindings.Secondary }],
|
||||
},
|
||||
{ toolName: toolNames.StackScrollMouseWheel, bindings: [] },
|
||||
],
|
||||
passive: [
|
||||
{ toolName: toolNames.Length },
|
||||
{ toolName: toolNames.ArrowAnnotate },
|
||||
{ toolName: toolNames.Bidirectional },
|
||||
{ toolName: toolNames.DragProbe },
|
||||
{ toolName: toolNames.EllipticalROI },
|
||||
{ toolName: toolNames.RectangleROI },
|
||||
{ toolName: toolNames.StackScroll },
|
||||
{ toolName: toolNames.Angle },
|
||||
{ toolName: toolNames.SegmentationDisplay },
|
||||
],
|
||||
disabled: [{ toolName: toolNames.Crosshairs }],
|
||||
|
||||
// enabled
|
||||
// disabled
|
||||
};
|
||||
|
||||
const toolsConfig = {
|
||||
[toolNames.Crosshairs]: {
|
||||
viewportIndicators: false,
|
||||
autoPan: {
|
||||
enabled: false,
|
||||
panSize: 10,
|
||||
},
|
||||
},
|
||||
[toolNames.ArrowAnnotate]: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
commandsManager.runCommand('arrowTextCallback', {
|
||||
callback,
|
||||
eventDetails,
|
||||
}),
|
||||
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
commandsManager.runCommand('arrowTextCallback', {
|
||||
callback,
|
||||
data,
|
||||
eventDetails,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
ToolGroupService.createToolGroupAndAddTools('mpr', tools, toolsConfig);
|
||||
}
|
||||
|
||||
function initToolGroups(extensionManager, ToolGroupService, commandsManager) {
|
||||
initDefaultToolGroup(
|
||||
extensionManager,
|
||||
@ -147,12 +216,7 @@ function initToolGroups(extensionManager, ToolGroupService, commandsManager) {
|
||||
'default'
|
||||
);
|
||||
initSRToolGroup(extensionManager, ToolGroupService, commandsManager);
|
||||
initDefaultToolGroup(
|
||||
extensionManager,
|
||||
ToolGroupService,
|
||||
commandsManager,
|
||||
'mpr'
|
||||
);
|
||||
initMPRToolGroup(extensionManager, ToolGroupService, commandsManager);
|
||||
}
|
||||
|
||||
export default initToolGroups;
|
||||
|
||||
@ -26,6 +26,18 @@ function _createButton(type, id, icon, label, commands, tooltip) {
|
||||
};
|
||||
}
|
||||
|
||||
function _createCommands(commandName, toolName, toolGroupIds) {
|
||||
return toolGroupIds.map(toolGroupId => ({
|
||||
/* It's a command that is being run when the button is clicked. */
|
||||
commandName,
|
||||
commandOptions: {
|
||||
toolName,
|
||||
toolGroupId,
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
}));
|
||||
}
|
||||
|
||||
const _createActionButton = _createButton.bind(null, 'action');
|
||||
const _createToggleButton = _createButton.bind(null, 'toggle');
|
||||
const _createToolButton = _createButton.bind(null, 'tool');
|
||||
@ -290,23 +302,41 @@ const toolbarButtons = [
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
// Todo: MPR not ready yet for SEG support, not activating it now
|
||||
// {
|
||||
// id: 'MPR',
|
||||
// type: 'ohif.action',
|
||||
// props: {
|
||||
// icon: 'old-play',
|
||||
// label: 'MPR',
|
||||
// type: 'action',
|
||||
// commands: [
|
||||
// {
|
||||
// commandName: 'setHangingProtocol',
|
||||
// commandOptions: { protocolId: 'mpr' },
|
||||
// context: 'CORNERSTONE',
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: 'MPR',
|
||||
type: 'ohif.action',
|
||||
props: {
|
||||
type: 'toggle',
|
||||
icon: 'icon-mpr',
|
||||
label: 'MPR',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleMPR',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Crosshairs',
|
||||
type: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: {
|
||||
toolGroupId: 'mpr',
|
||||
toolName: 'Crosshairs',
|
||||
},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// More...
|
||||
{
|
||||
id: 'MoreTools',
|
||||
@ -373,6 +403,25 @@ const toolbarButtons = [
|
||||
],
|
||||
'Flip Horizontal'
|
||||
),
|
||||
_createToggleButton('StackImageSync', 'link', 'Stack Image Sync', [
|
||||
{
|
||||
commandName: 'toggleStackImageSync',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]),
|
||||
_createToggleButton(
|
||||
'ReferenceLines',
|
||||
'tool-referenceLines', // change this with the new icon
|
||||
'Reference Lines',
|
||||
[
|
||||
{
|
||||
commandName: 'toggleReferenceLines',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
]
|
||||
),
|
||||
_createToolButton(
|
||||
'StackScroll',
|
||||
'tool-stack-scroll',
|
||||
|
||||
@ -140,6 +140,9 @@ function modeFactory({ modeConfiguration }) {
|
||||
SyncGroupService,
|
||||
MeasurementService,
|
||||
ToolBarService,
|
||||
SegmentationService,
|
||||
CornerstoneViewportService,
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
@ -147,6 +150,9 @@ function modeFactory({ modeConfiguration }) {
|
||||
MeasurementService.clearMeasurements();
|
||||
ToolGroupService.destroy();
|
||||
SyncGroupService.destroy();
|
||||
SegmentationService.destroy();
|
||||
CornerstoneViewportService.destroy();
|
||||
HangingProtocolService.reset();
|
||||
},
|
||||
validationTags: {
|
||||
study: [],
|
||||
@ -173,8 +179,8 @@ function modeFactory({ modeConfiguration }) {
|
||||
return {
|
||||
id: ohif.layout,
|
||||
props: {
|
||||
leftPanels: [],
|
||||
rightPanels: [tmtv.petSUV, tmtv.ROIThresholdPanel],
|
||||
// leftPanels: [ohif.thumbnailList],
|
||||
rightPanels: [tmtv.ROIThresholdPanel, tmtv.petSUV],
|
||||
viewports: [
|
||||
{
|
||||
namespace: cs3d.viewport,
|
||||
|
||||
@ -28,6 +28,7 @@ function initToolGroups(toolNames, Enums, ToolGroupService, commandsManager) {
|
||||
{ toolName: toolNames.ArrowAnnotate },
|
||||
{ toolName: toolNames.Bidirectional },
|
||||
{ toolName: toolNames.DragProbe },
|
||||
{ toolName: toolNames.Probe },
|
||||
{ toolName: toolNames.EllipticalROI },
|
||||
{ toolName: toolNames.RectangleROI },
|
||||
{ toolName: toolNames.StackScroll },
|
||||
|
||||
@ -228,11 +228,11 @@ const toolbarButtons = [
|
||||
id: 'Crosshairs',
|
||||
type: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'toggle',
|
||||
type: 'tool',
|
||||
icon: 'tool-crosshair',
|
||||
label: 'Crosshairs',
|
||||
commands: [
|
||||
..._createCommands('toggleCrosshairs', 'Crosshairs', [
|
||||
..._createCommands('setToolActive', 'Crosshairs', [
|
||||
toolGroupIds.CT,
|
||||
toolGroupIds.PT,
|
||||
toolGroupIds.Fusion,
|
||||
@ -272,7 +272,8 @@ const toolbarButtons = [
|
||||
commandName: 'displayNotification',
|
||||
commandOptions: {
|
||||
title: 'RectangleROI Threshold Tip',
|
||||
text: 'RectangleROI Threshold tool should be used on PT Axial Viewport',
|
||||
text:
|
||||
'RectangleROI Threshold tool should be used on PT Axial Viewport',
|
||||
type: 'info',
|
||||
},
|
||||
},
|
||||
|
||||
@ -56,7 +56,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.3",
|
||||
"@kitware/vtk.js": "^24.18.7",
|
||||
"@kitware/vtk.js": "25.9.0",
|
||||
"core-js": "^3.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
"moment": "^2.24.0",
|
||||
"query-string": "^6.14.0",
|
||||
"object-hash": "2.1.1",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"validate.js": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -33,7 +33,6 @@ describe('Top level exports', () => {
|
||||
'ToolBarService',
|
||||
'Types',
|
||||
'ViewportGridService',
|
||||
'SegmentationService',
|
||||
'HangingProtocolService',
|
||||
'UserAuthenticationService',
|
||||
'IWebApiDataSource',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user