commit
d5c347cecd
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 {
|
server {
|
||||||
listen 80;
|
listen ${PORT};
|
||||||
|
# listen 3000;
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html index.htm;
|
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" ]
|
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||||
then
|
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
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -39,3 +39,6 @@ screenshots/
|
|||||||
|
|
||||||
# autogenerated files
|
# autogenerated files
|
||||||
platform/viewer/src/pluginImports.js
|
platform/viewer/src/pluginImports.js
|
||||||
|
/Viewers.iml
|
||||||
|
platform/viewer/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
|
||||||
|
platform/viewer/.recipes/OpenResty-Orthanc/logs/*
|
||||||
|
|||||||
31
Dockerfile
31
Dockerfile
@ -21,24 +21,23 @@
|
|||||||
|
|
||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
# docker build -t ohif/viewer:latest .
|
# docker build -t ohif/viewer:latest .
|
||||||
FROM node:14.3.0-slim as json-copier
|
FROM node:16.15.0-slim as json-copier
|
||||||
|
|
||||||
RUN mkdir /usr/src/app
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY ["package.json", "yarn.lock", "preinstall.js", "./"]
|
||||||
COPY ["package.json", "yarn.lock", "./"]
|
|
||||||
COPY extensions /usr/src/app/extensions
|
COPY extensions /usr/src/app/extensions
|
||||||
COPY modes /usr/src/app/modes
|
COPY modes /usr/src/app/modes
|
||||||
COPY platform /usr/src/app/platform
|
COPY platform /usr/src/app/platform
|
||||||
|
|
||||||
# Find and remove non-package.json files
|
# Find and remove non-package.json files
|
||||||
RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
#RUN find extensions \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||||
RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
#RUN find modes \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||||
RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
#RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||||
|
|
||||||
# Copy Files
|
# Copy Files
|
||||||
FROM node:14.3.0-slim as builder
|
FROM node:16.15.0-slim as builder
|
||||||
RUN mkdir /usr/src/app
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
@ -46,12 +45,12 @@ COPY --from=json-copier /usr/src/app .
|
|||||||
|
|
||||||
# Run the install before copying the rest of the files
|
# Run the install before copying the rest of the files
|
||||||
RUN yarn config set workspaces-experimental true
|
RUN yarn config set workspaces-experimental true
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile --verbose
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# To restore workspaces symlinks
|
# To restore workspaces symlinks
|
||||||
RUN yarn install --frozen-lockfile
|
RUN yarn install --frozen-lockfile --verbose
|
||||||
|
|
||||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||||
ENV QUICK_BUILD true
|
ENV QUICK_BUILD true
|
||||||
@ -62,14 +61,14 @@ RUN yarn run build
|
|||||||
|
|
||||||
# Stage 3: Bundle the built application into a Docker container
|
# Stage 3: Bundle the built application into a Docker container
|
||||||
# which runs Nginx using Alpine Linux
|
# which runs Nginx using Alpine Linux
|
||||||
FROM nginx:1.15.5-alpine
|
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final
|
||||||
RUN apk add --no-cache bash
|
#RUN apk add --no-cache bash
|
||||||
RUN rm -rf /etc/nginx/conf.d
|
ENV PORT=3000
|
||||||
COPY .docker/Viewer-v2.x /etc/nginx/conf.d
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
COPY .docker/Viewer-v2.x/entrypoint.sh /usr/src/
|
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
|
RUN chmod 777 /usr/src/entrypoint.sh
|
||||||
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
|
COPY --from=builder /usr/src/app/platform/viewer/dist /usr/share/nginx/html
|
||||||
EXPOSE 80
|
|
||||||
EXPOSE 443
|
|
||||||
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@ -8,8 +8,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'inline-react-svg',
|
'inline-react-svg',
|
||||||
'@babel/plugin-proposal-class-properties',
|
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||||
'@babel/plugin-transform-typescript',
|
'@babel/plugin-transform-typescript',
|
||||||
|
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||||
|
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
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,434 @@
|
|||||||
|
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,
|
||||||
|
UINotificationService,
|
||||||
|
} = 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,
|
||||||
|
viewportId: viewportOptions.viewportId,
|
||||||
|
}}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.overlappingSegments) {
|
||||||
|
UINotificationService.show({
|
||||||
|
title: 'Overlapping Segments',
|
||||||
|
message:
|
||||||
|
'Overlapping segments detected which is not currently supported',
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
|
"@ohif/extension-cornerstone": "^3.0.0",
|
||||||
|
"@ohif/extension-measurement-tracking": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"dcmjs": "^0.24.5",
|
"dcmjs": "^0.28.3",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2"
|
||||||
"@ohif/extension-cornerstone": "^3.0.0",
|
|
||||||
"@ohif/extension-measurement-tracking": "^3.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.3",
|
"@babel/runtime": "7.16.3",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"@cornerstonejs/core": "^0.14.4",
|
"@cornerstonejs/core": "^0.21.5",
|
||||||
"@cornerstonejs/tools": "^0.21.7"
|
"@cornerstonejs/tools": "^0.29.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { adapters } from 'dcmjs';
|
|||||||
|
|
||||||
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D;
|
const { CodeScheme: Cornerstone3DCodeScheme } = adapters.Cornerstone3D;
|
||||||
|
|
||||||
const { ImageSet } = classes;
|
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
||||||
// TODO ->
|
// TODO ->
|
||||||
// Add SR thumbnail
|
// Add SR thumbnail
|
||||||
// Make viewport
|
// Make viewport
|
||||||
@ -16,6 +16,7 @@ const sopClassUids = [
|
|||||||
'1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
|
'1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
|
||||||
'1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
|
'1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
|
||||||
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
||||||
|
'1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
|
||||||
];
|
];
|
||||||
|
|
||||||
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
||||||
@ -44,6 +45,7 @@ const CodingSchemeDesignators = {
|
|||||||
|
|
||||||
const RELATIONSHIP_TYPE = {
|
const RELATIONSHIP_TYPE = {
|
||||||
INFERRED_FROM: 'INFERRED FROM',
|
INFERRED_FROM: 'INFERRED FROM',
|
||||||
|
CONTAINS: 'CONTAINS',
|
||||||
};
|
};
|
||||||
|
|
||||||
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||||
@ -77,6 +79,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
SeriesDate,
|
SeriesDate,
|
||||||
ConceptNameCodeSequence,
|
ConceptNameCodeSequence,
|
||||||
|
SOPClassUID,
|
||||||
} = instance;
|
} = instance;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -101,6 +104,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SOPClassHandlerId,
|
SOPClassHandlerId,
|
||||||
|
SOPClassUID,
|
||||||
referencedImages: null,
|
referencedImages: null,
|
||||||
measurements: null,
|
measurements: null,
|
||||||
isDerivedDisplaySet: true,
|
isDerivedDisplaySet: true,
|
||||||
@ -212,20 +216,26 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
newDisplaySet
|
newDisplaySet
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < images.length; i++) {
|
for (const imageId of imageIdsForDisplaySet) {
|
||||||
if (!unloadedMeasurements.length) {
|
if (!unloadedMeasurements.length) {
|
||||||
// All measurements loaded.
|
// All measurements loaded.
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = images[i];
|
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(
|
||||||
const { SOPInstanceUID } = image;
|
imageId
|
||||||
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
);
|
||||||
const imageId = imageIdsForDisplaySet[i];
|
|
||||||
|
|
||||||
|
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||||
const measurement = unloadedMeasurements[j];
|
const measurement = unloadedMeasurements[j];
|
||||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
|
if (
|
||||||
|
_measurementReferencesSOPInstanceUID(
|
||||||
|
measurement,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameNumber
|
||||||
|
)
|
||||||
|
) {
|
||||||
addMeasurement(
|
addMeasurement(
|
||||||
measurement,
|
measurement,
|
||||||
imageId,
|
imageId,
|
||||||
@ -239,9 +249,23 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
|
function _measurementReferencesSOPInstanceUID(
|
||||||
|
measurement,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameNumber
|
||||||
|
) {
|
||||||
const { coords } = measurement;
|
const { coords } = measurement;
|
||||||
|
|
||||||
|
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
|
||||||
|
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
||||||
|
const ReferencedFrameNumber =
|
||||||
|
(measurement.coords[0].ReferencedSOPSequence &&
|
||||||
|
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||||
|
1;
|
||||||
|
|
||||||
|
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber))
|
||||||
|
return false;
|
||||||
|
|
||||||
for (let j = 0; j < coords.length; j++) {
|
for (let j = 0; j < coords.length; j++) {
|
||||||
const coord = coords[j];
|
const coord = coords[j];
|
||||||
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
|
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
|
||||||
@ -376,7 +400,6 @@ function _processMeasurement(mergedContentSequence) {
|
|||||||
function _processTID1410Measurement(mergedContentSequence) {
|
function _processTID1410Measurement(mergedContentSequence) {
|
||||||
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
||||||
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
||||||
// TODO -> Look at RelationshipType => Contains means
|
|
||||||
|
|
||||||
const graphicItem = mergedContentSequence.find(
|
const graphicItem = mergedContentSequence.find(
|
||||||
group => group.ValueType === 'SCOORD'
|
group => group.ValueType === 'SCOORD'
|
||||||
@ -536,9 +559,14 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
|||||||
function _getCoordsFromSCOORDOrSCOORD3D(item) {
|
function _getCoordsFromSCOORDOrSCOORD3D(item) {
|
||||||
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
|
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
|
||||||
|
|
||||||
if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) {
|
if (
|
||||||
|
!(
|
||||||
|
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
|
||||||
|
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
|
||||||
|
)
|
||||||
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."`
|
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"`
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -597,12 +625,18 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
|||||||
|
|
||||||
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||||
const { ReferencedSOPSequence } = item;
|
const { ReferencedSOPSequence } = item;
|
||||||
const {
|
|
||||||
ReferencedSOPClassUID,
|
|
||||||
ReferencedSOPInstanceUID,
|
|
||||||
} = ReferencedSOPSequence;
|
|
||||||
|
|
||||||
referencedImages.push({ ReferencedSOPClassUID, ReferencedSOPInstanceUID });
|
if (item.hasOwnProperty('ReferencedSOPClassUID')) {
|
||||||
|
const {
|
||||||
|
ReferencedSOPClassUID,
|
||||||
|
ReferencedSOPInstanceUID,
|
||||||
|
} = ReferencedSOPSequence;
|
||||||
|
|
||||||
|
referencedImages.push({
|
||||||
|
ReferencedSOPClassUID,
|
||||||
|
ReferencedSOPInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return referencedImages;
|
return referencedImages;
|
||||||
|
|||||||
@ -99,7 +99,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
const annotation = filteredAnnotations[i];
|
const annotation = filteredAnnotations[i];
|
||||||
const annotationUID = annotation.annotationUID;
|
const annotationUID = annotation.annotationUID;
|
||||||
const { renderableData } = annotation.data.cachedStats;
|
const { renderableData } = annotation.data.cachedStats;
|
||||||
const { label, cachedStats } = annotation.data;
|
const { cachedStats } = annotation.data;
|
||||||
const { referencedImageId } = annotation.metadata;
|
const { referencedImageId } = annotation.metadata;
|
||||||
|
|
||||||
styleSpecifier.annotationUID = annotationUID;
|
styleSpecifier.annotationUID = annotationUID;
|
||||||
@ -121,22 +121,30 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
const renderableDataForGraphicType = renderableData[GraphicType];
|
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||||
|
|
||||||
let renderMethod;
|
let renderMethod;
|
||||||
|
let renderTextBox;
|
||||||
|
let canvasCoordinatesAdapter;
|
||||||
|
|
||||||
switch (GraphicType) {
|
switch (GraphicType) {
|
||||||
case SCOORD_TYPES.POINT:
|
case SCOORD_TYPES.POINT:
|
||||||
renderMethod = this.renderPoint;
|
renderMethod = this.renderPoint;
|
||||||
|
renderTextBox = this.renderTextBox;
|
||||||
break;
|
break;
|
||||||
case SCOORD_TYPES.MULTIPOINT:
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
renderMethod = this.renderMultipoint;
|
renderMethod = this.renderMultipoint;
|
||||||
|
renderTextBox = this.renderTextBox;
|
||||||
break;
|
break;
|
||||||
case SCOORD_TYPES.POLYLINE:
|
case SCOORD_TYPES.POLYLINE:
|
||||||
renderMethod = this.renderPolyLine;
|
renderMethod = this.renderPolyLine;
|
||||||
break;
|
break;
|
||||||
case SCOORD_TYPES.CIRCLE:
|
case SCOORD_TYPES.CIRCLE:
|
||||||
renderMethod = this.renderEllipse;
|
renderMethod = this.renderEllipse;
|
||||||
|
renderTextBox = this.renderTextBox;
|
||||||
break;
|
break;
|
||||||
case SCOORD_TYPES.ELLIPSE:
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
renderMethod = this.renderEllipse;
|
renderMethod = this.renderEllipse;
|
||||||
|
renderTextBox = this.renderTextBox;
|
||||||
|
canvasCoordinatesAdapter =
|
||||||
|
utilities.math.ellipse.getCanvasEllipseCorners;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported GraphicType: ${GraphicType}`);
|
throw new Error(`Unsupported GraphicType: ${GraphicType}`);
|
||||||
@ -151,60 +159,18 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
options
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!canvasCoordinates) {
|
if (typeof renderTextBox === 'function') {
|
||||||
return;
|
renderTextBox.call(
|
||||||
}
|
this,
|
||||||
|
svgDrawingHelper,
|
||||||
const textLines = this._getTextBoxLinesFromLabels(label);
|
viewport,
|
||||||
|
canvasCoordinates,
|
||||||
let canvasCornersToUseForTextBox = canvasCoordinates;
|
canvasCoordinatesAdapter,
|
||||||
|
annotation,
|
||||||
if (GraphicType === SCOORD_TYPES.ELLIPSE) {
|
styleSpecifier,
|
||||||
canvasCornersToUseForTextBox = utilities.math.ellipse.getCanvasEllipseCorners(
|
options
|
||||||
canvasCoordinates
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(
|
|
||||||
canvasCornersToUseForTextBox
|
|
||||||
);
|
|
||||||
|
|
||||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(
|
|
||||||
canvasTextBoxCoords
|
|
||||||
);
|
|
||||||
|
|
||||||
const textBoxPosition = viewport.worldToCanvas(
|
|
||||||
annotation.data.handles.textBox.worldPosition
|
|
||||||
);
|
|
||||||
|
|
||||||
const textBoxUID = '1';
|
|
||||||
const textBoxOptions = this.getLinkedTextBoxStyle(
|
|
||||||
styleSpecifier,
|
|
||||||
annotation
|
|
||||||
);
|
|
||||||
|
|
||||||
const boundingBox = drawing.drawLinkedTextBox(
|
|
||||||
svgDrawingHelper,
|
|
||||||
annotationUID,
|
|
||||||
textBoxUID,
|
|
||||||
textLines,
|
|
||||||
textBoxPosition,
|
|
||||||
canvasCoordinates,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
...textBoxOptions,
|
|
||||||
color,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { x: left, y: top, width, height } = boundingBox;
|
|
||||||
|
|
||||||
annotation.data.handles.textBox.worldBoundingBox = {
|
|
||||||
topLeft: viewport.canvasToWorld([left, top]),
|
|
||||||
topRight: viewport.canvasToWorld([left + width, top]),
|
|
||||||
bottomLeft: viewport.canvasToWorld([left, top + height]),
|
|
||||||
bottomRight: viewport.canvasToWorld([left + width, top + height]),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -217,28 +183,32 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
referencedImageId,
|
referencedImageId,
|
||||||
options
|
options
|
||||||
) {
|
) {
|
||||||
// Todo: this needs to use the drawPolyLine from cs3D since it is implemented
|
const drawingOptions = {
|
||||||
// now, before it was implemented with a loop over drawLine which is hacky
|
color: options.color,
|
||||||
|
width: options.lineWidth,
|
||||||
|
};
|
||||||
let canvasCoordinates;
|
let canvasCoordinates;
|
||||||
renderableData.map((data, index) => {
|
renderableData.map((data, index) => {
|
||||||
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
|
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
|
||||||
|
|
||||||
|
const lineUID = `${index}`;
|
||||||
if (canvasCoordinates.length === 2) {
|
if (canvasCoordinates.length === 2) {
|
||||||
const lineUID = `${index}`;
|
|
||||||
drawing.drawLine(
|
drawing.drawLine(
|
||||||
svgDrawingHelper,
|
svgDrawingHelper,
|
||||||
annotationUID,
|
annotationUID,
|
||||||
lineUID,
|
lineUID,
|
||||||
canvasCoordinates[0],
|
canvasCoordinates[0],
|
||||||
canvasCoordinates[1],
|
canvasCoordinates[1],
|
||||||
{
|
drawingOptions
|
||||||
color: options.color,
|
|
||||||
width: options.lineWidth,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Drawing polyline for SR not yet implemented');
|
drawing.drawPolyline(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
lineUID,
|
||||||
|
canvasCoordinates,
|
||||||
|
drawingOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -367,6 +337,71 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
|||||||
|
|
||||||
return canvasCoordinates;
|
return canvasCoordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderTextBox(
|
||||||
|
svgDrawingHelper,
|
||||||
|
viewport,
|
||||||
|
canvasCoordinates,
|
||||||
|
canvasCoordinatesAdapter,
|
||||||
|
annotation,
|
||||||
|
styleSpecifier,
|
||||||
|
options = {}
|
||||||
|
) {
|
||||||
|
if (!canvasCoordinates || !annotation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { annotationUID, data = {} } = annotation;
|
||||||
|
const { label } = data;
|
||||||
|
const { color } = options;
|
||||||
|
|
||||||
|
let adaptedCanvasCoordinates = canvasCoordinates;
|
||||||
|
// adapt coordinates if there is an adapter
|
||||||
|
if (typeof canvasCoordinatesAdapter === 'function') {
|
||||||
|
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
|
||||||
|
}
|
||||||
|
const textLines = this._getTextBoxLinesFromLabels(label);
|
||||||
|
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(
|
||||||
|
adaptedCanvasCoordinates
|
||||||
|
);
|
||||||
|
|
||||||
|
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(
|
||||||
|
canvasTextBoxCoords
|
||||||
|
);
|
||||||
|
|
||||||
|
const textBoxPosition = viewport.worldToCanvas(
|
||||||
|
annotation.data.handles.textBox.worldPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
const textBoxUID = '1';
|
||||||
|
const textBoxOptions = this.getLinkedTextBoxStyle(
|
||||||
|
styleSpecifier,
|
||||||
|
annotation
|
||||||
|
);
|
||||||
|
|
||||||
|
const boundingBox = drawing.drawLinkedTextBox(
|
||||||
|
svgDrawingHelper,
|
||||||
|
annotationUID,
|
||||||
|
textBoxUID,
|
||||||
|
textLines,
|
||||||
|
textBoxPosition,
|
||||||
|
canvasCoordinates,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
...textBoxOptions,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const { x: left, y: top, width, height } = boundingBox;
|
||||||
|
|
||||||
|
annotation.data.handles.textBox.worldBoundingBox = {
|
||||||
|
topLeft: viewport.canvasToWorld([left, top]),
|
||||||
|
topRight: viewport.canvasToWorld([left + width, top]),
|
||||||
|
bottomLeft: viewport.canvasToWorld([left, top + height]),
|
||||||
|
bottomRight: viewport.canvasToWorld([left + width, top + height]),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHORT_HAND_MAP = {
|
const SHORT_HAND_MAP = {
|
||||||
|
|||||||
@ -45,6 +45,12 @@ export default function addMeasurement(
|
|||||||
|
|
||||||
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
||||||
|
|
||||||
|
// Create Cornerstone3D Annotation from measurement
|
||||||
|
const frameNumber =
|
||||||
|
(measurement.coords[0].ReferencedSOPSequence &&
|
||||||
|
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||||
|
1;
|
||||||
|
|
||||||
const SRAnnotation: Types.Annotation = {
|
const SRAnnotation: Types.Annotation = {
|
||||||
annotationUID: measurement.TrackingUniqueIdentifier,
|
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||||
metadata: {
|
metadata: {
|
||||||
@ -61,6 +67,7 @@ export default function addMeasurement(
|
|||||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||||
renderableData: measurementData.renderableData,
|
renderableData: measurementData.renderableData,
|
||||||
},
|
},
|
||||||
|
frameNumber: frameNumber,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -75,6 +82,7 @@ export default function addMeasurement(
|
|||||||
// It'd be super weird if it didn't anyway as a SCOORD.
|
// It'd be super weird if it didn't anyway as a SCOORD.
|
||||||
measurement.ReferencedSOPInstanceUID =
|
measurement.ReferencedSOPInstanceUID =
|
||||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||||
|
measurement.frameNumber = frameNumber;
|
||||||
delete measurement.coords;
|
delete measurement.coords;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,18 @@
|
|||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import OHIF, { utils } from '@ohif/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Notification,
|
Notification,
|
||||||
ViewportActionBar,
|
ViewportActionBar,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
useViewportDialog,
|
useViewportDialog,
|
||||||
|
Tooltip,
|
||||||
|
Icon,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
|
|
||||||
@ -27,6 +32,8 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
extensionManager,
|
extensionManager,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const { t } = useTranslation('SRViewport');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
@ -318,8 +325,6 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Modality } = srDisplaySet;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PatientID,
|
PatientID,
|
||||||
PatientName,
|
PatientName,
|
||||||
@ -341,24 +346,23 @@ function OHIFCornerstoneSRViewport(props) {
|
|||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
}}
|
}}
|
||||||
onPillClick={() => {
|
onArrowsClick={onMeasurementChange}
|
||||||
sendTrackedMeasurementsEvent('RESTORE_PROMPT_HYDRATE_SR', {
|
getStatusComponent={() =>
|
||||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
_getStatusComponent({
|
||||||
|
srDisplaySet,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
});
|
isTracked: false,
|
||||||
}}
|
isRehydratable: srDisplaySet.isRehydratable,
|
||||||
onSeriesChange={onMeasurementChange}
|
isLocked,
|
||||||
|
sendTrackedMeasurementsEvent,
|
||||||
|
})
|
||||||
|
}
|
||||||
studyData={{
|
studyData={{
|
||||||
label: viewportLabel,
|
label: viewportLabel,
|
||||||
useAltStyling: true,
|
useAltStyling: true,
|
||||||
isTracked: false,
|
|
||||||
isLocked,
|
|
||||||
isRehydratable: srDisplaySet.isRehydratable,
|
|
||||||
isHydrated,
|
|
||||||
studyDate: formatDate(StudyDate),
|
studyDate: formatDate(StudyDate),
|
||||||
currentSeries: SeriesNumber,
|
currentSeries: SeriesNumber,
|
||||||
seriesDescription: SeriesDescription,
|
seriesDescription: SeriesDescription,
|
||||||
modality: Modality,
|
|
||||||
patientInformation: {
|
patientInformation: {
|
||||||
patientName: PatientName
|
patientName: PatientName
|
||||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||||
@ -439,6 +443,139 @@ async function _getViewportReferencedDisplaySetData(
|
|||||||
return { referencedDisplaySetMetadata, referencedDisplaySet };
|
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() {
|
// function _onDoubleClick() {
|
||||||
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
// const cancelActiveManipulatorsForElement = cornerstoneTools.getModule(
|
||||||
// 'manipulatorState'
|
// 'manipulatorState'
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"cornerstone-wado-image-loader": "^4.2.1",
|
"cornerstone-wado-image-loader": "^4.2.1",
|
||||||
"dcmjs": "^0.24.5",
|
"dcmjs": "^0.28.3",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
@ -43,11 +43,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.17.9",
|
"@babel/runtime": "7.17.9",
|
||||||
"@cornerstonejs/core": "^0.14.4",
|
"@cornerstonejs/core": "^0.21.5",
|
||||||
"@cornerstonejs/streaming-image-volume-loader": "^0.4.16",
|
"@cornerstonejs/streaming-image-volume-loader": "^0.6.5",
|
||||||
"@cornerstonejs/tools": "^0.21.7",
|
"@cornerstonejs/tools": "^0.29.8",
|
||||||
"@kitware/vtk.js": "^24.18.7",
|
"@kitware/vtk.js": "25.9.0",
|
||||||
"dom-to-image": "^2.6.0",
|
"html2canvas": "^1.4.1",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"shader-loader": "^1.3.1",
|
"shader-loader": "^1.3.1",
|
||||||
|
|||||||
@ -1,15 +1,25 @@
|
|||||||
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
import React, { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useViewportGrid } from '@ohif/ui';
|
|
||||||
import * as cs3DTools from '@cornerstonejs/tools';
|
import * as cs3DTools from '@cornerstonejs/tools';
|
||||||
import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
import {
|
||||||
|
Enums,
|
||||||
|
eventTarget,
|
||||||
|
getEnabledElement,
|
||||||
|
StackViewport,
|
||||||
|
utilities as csUtils,
|
||||||
|
CONSTANTS,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
|
|
||||||
import { setEnabledElement } from '../state';
|
import { setEnabledElement } from '../state';
|
||||||
import CornerstoneCacheService from '../services/ViewportService/CornerstoneCacheService';
|
|
||||||
|
|
||||||
import './OHIFCornerstoneViewport.css';
|
import './OHIFCornerstoneViewport.css';
|
||||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||||
|
import {
|
||||||
|
IStackViewport,
|
||||||
|
IVolumeViewport,
|
||||||
|
} from '@cornerstonejs/core/dist/esm/types';
|
||||||
|
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
const STACK = 'stack';
|
const STACK = 'stack';
|
||||||
|
|
||||||
@ -22,24 +32,65 @@ function areEqual(prevProps, nextProps) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevDisplaySets = prevProps.displaySets[0];
|
if (
|
||||||
const nextDisplaySets = nextProps.displaySets[0];
|
prevProps.viewportOptions.orientation !==
|
||||||
|
nextProps.viewportOptions.orientation
|
||||||
if (prevDisplaySets && nextDisplaySets) {
|
) {
|
||||||
const areSameDisplaySetInstanceUIDs =
|
return false;
|
||||||
prevDisplaySets.displaySetInstanceUID ===
|
|
||||||
nextDisplaySets.displaySetInstanceUID;
|
|
||||||
const areSameImageLength =
|
|
||||||
prevDisplaySets.images.length === nextDisplaySets.images.length;
|
|
||||||
const areSameImageIds = prevDisplaySets.images.every(
|
|
||||||
(prevImage, index) =>
|
|
||||||
prevImage.imageId === nextDisplaySets.images[index].imageId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
areSameDisplaySetInstanceUIDs && areSameImageLength && areSameImageIds
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
if (
|
||||||
|
prevProps.viewportOptions.toolGroupId !==
|
||||||
|
nextProps.viewportOptions.toolGroupId
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.viewportOptions.viewportType !==
|
||||||
|
nextProps.viewportOptions.viewportType
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevDisplaySets = prevProps.displaySets;
|
||||||
|
const nextDisplaySets = nextProps.displaySets;
|
||||||
|
|
||||||
|
if (prevDisplaySets.length !== nextDisplaySets.length) {
|
||||||
|
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
|
// Todo: This should be done with expose of internal API similar to react-vtkjs-viewport
|
||||||
@ -61,8 +112,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||||
const [viewportData, setViewportData] = useState(null);
|
|
||||||
const [_, viewportGridService] = useViewportGrid();
|
|
||||||
|
|
||||||
const elementRef = useRef();
|
const elementRef = useRef();
|
||||||
|
|
||||||
@ -73,6 +122,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
ToolGroupService,
|
ToolGroupService,
|
||||||
SyncGroupService,
|
SyncGroupService,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
|
CornerstoneCacheService,
|
||||||
|
ViewportGridService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
// useCallback for scroll bar height calculation
|
// useCallback for scroll bar height calculation
|
||||||
@ -89,6 +140,28 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
}
|
}
|
||||||
}, [elementRef]);
|
}, [elementRef]);
|
||||||
|
|
||||||
|
const cleanUpServices = useCallback(() => {
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!viewportInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportId = viewportInfo.getViewportId();
|
||||||
|
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||||
|
const syncGroups = viewportInfo.getSyncGroups();
|
||||||
|
|
||||||
|
ToolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||||
|
|
||||||
|
SyncGroupService.removeViewportFromSyncGroup(
|
||||||
|
viewportId,
|
||||||
|
renderingEngineId,
|
||||||
|
syncGroups
|
||||||
|
);
|
||||||
|
}, [viewportIndex, viewportOptions.viewportId]);
|
||||||
|
|
||||||
const elementEnabledHandler = useCallback(
|
const elementEnabledHandler = useCallback(
|
||||||
evt => {
|
evt => {
|
||||||
// check this is this element reference and return early if doesn't match
|
// check this is this element reference and return early if doesn't match
|
||||||
@ -129,7 +202,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
|
|
||||||
// disable the element upon unmounting
|
// disable the element upon unmounting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
CornerstoneViewportService.enableElement(
|
CornerstoneViewportService.enableViewport(
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
elementRef.current
|
elementRef.current
|
||||||
@ -143,31 +216,22 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
setImageScrollBarHeight();
|
setImageScrollBarHeight();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
cleanUpServices();
|
||||||
viewportIndex
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewportId = viewportInfo.getViewportId();
|
|
||||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
|
||||||
const syncGroups = viewportInfo.getSyncGroups();
|
|
||||||
|
|
||||||
ToolGroupService.disable(viewportId, renderingEngineId);
|
|
||||||
SyncGroupService.removeViewportFromSyncGroup(
|
|
||||||
viewportId,
|
|
||||||
renderingEngineId,
|
|
||||||
syncGroups
|
|
||||||
);
|
|
||||||
|
|
||||||
CornerstoneViewportService.disableElement(viewportIndex);
|
CornerstoneViewportService.disableElement(viewportIndex);
|
||||||
|
|
||||||
|
if (onElementDisabled) {
|
||||||
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
onElementDisabled(viewportInfo);
|
||||||
|
}
|
||||||
|
|
||||||
eventTarget.removeEventListener(
|
eventTarget.removeEventListener(
|
||||||
Enums.Events.ELEMENT_ENABLED,
|
Enums.Events.ELEMENT_ENABLED,
|
||||||
elementEnabledHandler
|
elementEnabledHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onElementDisabled) {
|
|
||||||
onElementDisabled();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -183,11 +247,12 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
const { unsubscribe } = DisplaySetService.subscribe(
|
const { unsubscribe } = DisplaySetService.subscribe(
|
||||||
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
DisplaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||||
async invalidatedDisplaySetInstanceUID => {
|
async invalidatedDisplaySetInstanceUID => {
|
||||||
if (
|
const viewportInfo = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
viewportData.displaySetInstanceUIDs.includes(
|
viewportIndex
|
||||||
invalidatedDisplaySetInstanceUID
|
);
|
||||||
)
|
|
||||||
) {
|
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
||||||
|
const viewportData = viewportInfo.getViewportData();
|
||||||
const newViewportData = await CornerstoneCacheService.invalidateViewportData(
|
const newViewportData = await CornerstoneCacheService.invalidateViewportData(
|
||||||
viewportData,
|
viewportData,
|
||||||
invalidatedDisplaySetInstanceUID,
|
invalidatedDisplaySetInstanceUID,
|
||||||
@ -201,15 +266,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
newViewportData,
|
newViewportData,
|
||||||
keepCamera
|
keepCamera
|
||||||
);
|
);
|
||||||
|
|
||||||
setViewportData(newViewportData);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [viewportData, viewportIndex]);
|
}, [viewportIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// handle the default viewportType to be stack
|
// handle the default viewportType to be stack
|
||||||
@ -218,22 +281,19 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadViewportData = async () => {
|
const loadViewportData = async () => {
|
||||||
const viewportData = await CornerstoneCacheService.getViewportData(
|
const viewportData = await CornerstoneCacheService.createViewportData(
|
||||||
viewportIndex,
|
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportOptions.viewportType,
|
viewportOptions,
|
||||||
dataSource,
|
dataSource,
|
||||||
initialImageIndex
|
initialImageIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
CornerstoneViewportService.setViewportDisplaySets(
|
CornerstoneViewportService.setViewportData(
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
viewportData,
|
viewportData,
|
||||||
viewportOptions,
|
viewportOptions,
|
||||||
displaySetOptions
|
displaySetOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
setViewportData(viewportData);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadViewportData();
|
loadViewportData();
|
||||||
@ -256,7 +316,8 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
elementRef,
|
elementRef,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
);
|
);
|
||||||
|
|
||||||
_checkForCachedJumpToMeasurementEvents(
|
_checkForCachedJumpToMeasurementEvents(
|
||||||
@ -265,13 +326,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
elementRef,
|
elementRef,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribeFromJumpToMeasurementEvents();
|
unsubscribeFromJumpToMeasurementEvents();
|
||||||
};
|
};
|
||||||
}, [displaySets, elementRef, viewportIndex, viewportData]);
|
}, [displaySets, elementRef, viewportIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="viewport-wrapper">
|
<div className="viewport-wrapper">
|
||||||
@ -280,7 +342,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
|||||||
handleHeight
|
handleHeight
|
||||||
skipOnMount={true} // Todo: make these configurable
|
skipOnMount={true} // Todo: make these configurable
|
||||||
refreshMode={'debounce'}
|
refreshMode={'debounce'}
|
||||||
refreshRate={100}
|
refreshRate={200} // transition amount in side panel
|
||||||
onResize={onResize}
|
onResize={onResize}
|
||||||
targetRef={elementRef.current}
|
targetRef={elementRef.current}
|
||||||
/>
|
/>
|
||||||
@ -308,7 +370,8 @@ function _subscribeToJumpToMeasurementEvents(
|
|||||||
elementRef,
|
elementRef,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
) {
|
) {
|
||||||
const displaysUIDs = displaySets.map(
|
const displaysUIDs = displaySets.map(
|
||||||
displaySet => displaySet.displaySetInstanceUID
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
@ -326,7 +389,8 @@ function _subscribeToJumpToMeasurementEvents(
|
|||||||
viewportIndex,
|
viewportIndex,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,7 +406,8 @@ function _checkForCachedJumpToMeasurementEvents(
|
|||||||
elementRef,
|
elementRef,
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
displaySets,
|
displaySets,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
) {
|
) {
|
||||||
const displaysUIDs = displaySets.map(
|
const displaysUIDs = displaySets.map(
|
||||||
displaySet => displaySet.displaySetInstanceUID
|
displaySet => displaySet.displaySetInstanceUID
|
||||||
@ -365,7 +430,8 @@ function _checkForCachedJumpToMeasurementEvents(
|
|||||||
viewportIndex,
|
viewportIndex,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,10 +443,11 @@ function _jumpToMeasurement(
|
|||||||
viewportIndex,
|
viewportIndex,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
viewportGridService
|
ViewportGridService,
|
||||||
|
CornerstoneViewportService
|
||||||
) {
|
) {
|
||||||
const targetElement = targetElementRef.current;
|
const targetElement = targetElementRef.current;
|
||||||
const { displaySetInstanceUID, SOPInstanceUID } = measurement;
|
const { displaySetInstanceUID, SOPInstanceUID, frameNumber } = measurement;
|
||||||
|
|
||||||
if (!SOPInstanceUID) {
|
if (!SOPInstanceUID) {
|
||||||
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
console.warn('cannot jump in a non-acquisition plane measurements yet');
|
||||||
@ -390,17 +457,63 @@ function _jumpToMeasurement(
|
|||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageIdIndex = referencedDisplaySet.images.findIndex(
|
|
||||||
i => i.SOPInstanceUID === SOPInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
||||||
// to set it properly
|
// to set it properly
|
||||||
// setCornerstoneMeasurementActive(measurement);
|
// 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();
|
||||||
|
imageIdIndex = imageIds.findIndex(imageId => {
|
||||||
|
const {
|
||||||
|
SOPInstanceUID: aSOPInstanceUID,
|
||||||
|
frameNumber: aFrameNumber,
|
||||||
|
} = getSOPInstanceAttributes(imageId);
|
||||||
|
return (
|
||||||
|
aSOPInstanceUID === SOPInstanceUID &&
|
||||||
|
(!frameNumber || frameNumber === aFrameNumber)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
if (getEnabledElement(targetElement)) {
|
|
||||||
cs3DTools.utilities.jumpToSlice(targetElement, {
|
cs3DTools.utilities.jumpToSlice(targetElement, {
|
||||||
imageIndex: imageIdIndex,
|
imageIndex: imageIdIndex,
|
||||||
});
|
});
|
||||||
@ -419,7 +532,7 @@ OHIFCornerstoneViewport.propTypes = {
|
|||||||
displaySets: PropTypes.array.isRequired,
|
displaySets: PropTypes.array.isRequired,
|
||||||
dataSource: PropTypes.object.isRequired,
|
dataSource: PropTypes.object.isRequired,
|
||||||
viewportOptions: PropTypes.object,
|
viewportOptions: PropTypes.object,
|
||||||
displaySetOptions: PropTypes.arrayOf(PropTypes.object),
|
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
|
||||||
servicesManager: PropTypes.object.isRequired,
|
servicesManager: PropTypes.object.isRequired,
|
||||||
onElementEnabled: PropTypes.func,
|
onElementEnabled: PropTypes.func,
|
||||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import ViewportImageScrollbar from './ViewportImageScrollbar';
|
import ViewportImageScrollbar from './ViewportImageScrollbar';
|
||||||
import ViewportOverlay from './ViewportOverlay';
|
import ViewportOverlay from './ViewportOverlay';
|
||||||
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||||
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
||||||
import CornerstoneCacheService from '../../services/ViewportService/CornerstoneCacheService';
|
|
||||||
|
|
||||||
function CornerstoneOverlays(props) {
|
function CornerstoneOverlays(props) {
|
||||||
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
|
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
|
||||||
@ -16,8 +15,8 @@ function CornerstoneOverlays(props) {
|
|||||||
const [viewportData, setViewportData] = useState(null);
|
const [viewportData, setViewportData] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = CornerstoneCacheService.subscribe(
|
const { unsubscribe } = CornerstoneViewportService.subscribe(
|
||||||
CornerstoneCacheService.EVENTS.VIEWPORT_DATA_CHANGED,
|
CornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||||
props => {
|
props => {
|
||||||
if (props.viewportIndex !== viewportIndex) {
|
if (props.viewportIndex !== viewportIndex) {
|
||||||
return;
|
return;
|
||||||
@ -47,7 +46,7 @@ function CornerstoneOverlays(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="noselect">
|
||||||
<ViewportImageScrollbar
|
<ViewportImageScrollbar
|
||||||
viewportIndex={viewportIndex}
|
viewportIndex={viewportIndex}
|
||||||
viewportData={viewportData}
|
viewportData={viewportData}
|
||||||
@ -64,11 +63,15 @@ function CornerstoneOverlays(props) {
|
|||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
element={element}
|
element={element}
|
||||||
/>
|
/>
|
||||||
<ViewportLoadingIndicator viewportData={viewportData} element={element} />
|
<ViewportImageSliceLoadingIndicator
|
||||||
|
viewportData={viewportData}
|
||||||
|
element={element}
|
||||||
|
/>
|
||||||
<ViewportOrientationMarkers
|
<ViewportOrientationMarkers
|
||||||
imageSliceData={imageSliceData}
|
imageSliceData={imageSliceData}
|
||||||
element={element}
|
element={element}
|
||||||
viewportData={viewportData}
|
viewportData={viewportData}
|
||||||
|
servicesManager={servicesManager}
|
||||||
viewportIndex={viewportIndex}
|
viewportIndex={viewportIndex}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -25,9 +25,13 @@ function CornerstoneImageScrollbar({
|
|||||||
viewportId
|
viewportId
|
||||||
);
|
);
|
||||||
|
|
||||||
// on image scrollbar change, stop the CINE if it is playing
|
const { isCineEnabled } = CineService.getState();
|
||||||
CineService.stopClip(element);
|
|
||||||
CineService.setCine({ id: viewportIndex, isPlaying: false });
|
if (isCineEnabled) {
|
||||||
|
// on image scrollbar change, stop the CINE if it is playing
|
||||||
|
CineService.stopClip(element);
|
||||||
|
CineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||||
|
}
|
||||||
|
|
||||||
csToolsUtils.jumpToSlice(viewport.element, {
|
csToolsUtils.jumpToSlice(viewport.element, {
|
||||||
imageIndex,
|
imageIndex,
|
||||||
@ -53,7 +57,7 @@ function CornerstoneImageScrollbar({
|
|||||||
|
|
||||||
setImageSliceData({
|
setImageSliceData({
|
||||||
imageIndex: imageIndex,
|
imageIndex: imageIndex,
|
||||||
numberOfSlices: viewportData.imageIds.length,
|
numberOfSlices: viewportData.data.imageIds.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -83,7 +87,7 @@ function CornerstoneImageScrollbar({
|
|||||||
// find the index of imageId in the imageIds
|
// find the index of imageId in the imageIds
|
||||||
setImageSliceData({
|
setImageSliceData({
|
||||||
imageIndex: newImageIdIndex,
|
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 PropTypes from 'prop-types';
|
||||||
import { Enums } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
|
|
||||||
function ViewportLoadingIndicator({ viewportData, element }) {
|
function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
@ -89,15 +89,15 @@ function ViewportLoadingIndicator({ viewportData, element }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewportLoadingIndicator.propTypes = {
|
ViewportImageSliceLoadingIndicator.propTypes = {
|
||||||
percentComplete: PropTypes.number,
|
percentComplete: PropTypes.number,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
element: PropTypes.object,
|
element: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewportLoadingIndicator.defaultProps = {
|
ViewportImageSliceLoadingIndicator.defaultProps = {
|
||||||
percentComplete: 0,
|
percentComplete: 0,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ViewportLoadingIndicator;
|
export default ViewportImageSliceLoadingIndicator;
|
||||||
@ -4,14 +4,13 @@
|
|||||||
--scrollbar-width: 20px;
|
--scrollbar-width: 20px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #ccc;
|
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .orientation-marker {
|
.ViewportOrientationMarkers .orientation-marker {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .top-mid {
|
.ViewportOrientationMarkers .top-mid {
|
||||||
top: 5px;
|
top: 0.6rem;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .left-mid {
|
.ViewportOrientationMarkers .left-mid {
|
||||||
@ -23,7 +22,7 @@
|
|||||||
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
left: calc(100% - var(--marker-width) - var(--scrollbar-width));
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .bottom-mid {
|
.ViewportOrientationMarkers .bottom-mid {
|
||||||
top: calc(100% - var(--marker-height) - 5px);
|
top: calc(100% - var(--marker-height) - 0.6rem);
|
||||||
left: 47%;
|
left: 47%;
|
||||||
}
|
}
|
||||||
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
.ViewportOrientationMarkers .right-mid .orientation-marker-value {
|
||||||
|
|||||||
@ -1,10 +1,164 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import classNames from 'classnames';
|
||||||
import { metaData, Enums, Types } from '@cornerstonejs/core';
|
import {
|
||||||
|
metaData,
|
||||||
|
Enums,
|
||||||
|
Types,
|
||||||
|
getEnabledElement,
|
||||||
|
utilities as csUtils,
|
||||||
|
} from '@cornerstonejs/core';
|
||||||
import { utilities } from '@cornerstonejs/tools';
|
import { utilities } from '@cornerstonejs/tools';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
|
|
||||||
import './ViewportOrientationMarkers.css';
|
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;
|
||||||
|
|
||||||
|
// Todo: probably this can be done in a better way in which we identify bright
|
||||||
|
// background
|
||||||
|
const isLight = backgroundColor
|
||||||
|
? csUtils.isEqual(backgroundColor, [1, 1, 1])
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return orientationMarkers.map((m, index) => (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
`${m}-mid orientation-marker`,
|
||||||
|
isLight ? 'text-[#726F7E]' : 'text-[#ccc]'
|
||||||
|
)}
|
||||||
|
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 +170,13 @@ import { getEnabledElement } from '../../state';
|
|||||||
* @param {*} rotation in degrees
|
* @param {*} rotation in degrees
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function getOrientationMarkers(
|
function _getOrientationMarkers(
|
||||||
rowCosines,
|
rowCosines,
|
||||||
columnCosines,
|
columnCosines,
|
||||||
rotation,
|
rotation,
|
||||||
flipVertical,
|
flipVertical,
|
||||||
flipHorizontal
|
flipHorizontal
|
||||||
) {
|
) {
|
||||||
const {
|
|
||||||
getOrientationStringLPS,
|
|
||||||
invertOrientationStringLPS,
|
|
||||||
} = utilities.orientation;
|
|
||||||
const rowString = getOrientationStringLPS(rowCosines);
|
const rowString = getOrientationStringLPS(rowCosines);
|
||||||
const columnString = getOrientationStringLPS(columnCosines);
|
const columnString = getOrientationStringLPS(columnCosines);
|
||||||
const oppositeRowString = invertOrientationStringLPS(rowString);
|
const oppositeRowString = invertOrientationStringLPS(rowString);
|
||||||
@ -79,117 +229,4 @@ function getOrientationMarkers(
|
|||||||
return markers;
|
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;
|
export default ViewportOrientationMarkers;
|
||||||
|
|||||||
@ -28,6 +28,25 @@ function CornerstoneViewportOverlay({
|
|||||||
setActiveTools(ToolBarService.getActiveTools());
|
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
|
* Updating the VOI when the viewport changes its voi
|
||||||
*/
|
*/
|
||||||
@ -100,23 +119,6 @@ function CornerstoneViewportOverlay({
|
|||||||
};
|
};
|
||||||
}, [viewportIndex, viewportData]);
|
}, [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 getTopLeftContent = useCallback(() => {
|
||||||
const { windowWidth, windowCenter } = voi;
|
const { windowWidth, windowCenter } = voi;
|
||||||
|
|
||||||
@ -186,22 +188,33 @@ function CornerstoneViewportOverlay({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewportData.imageIds.length === 0) {
|
const ohifViewport = CornerstoneViewportService.getViewportInfoByIndex(
|
||||||
throw new Error(
|
viewportIndex
|
||||||
'ViewportOverlay: only viewports with imageIds is supported at this time'
|
);
|
||||||
);
|
|
||||||
|
if (!ohifViewport) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backgroundColor = ohifViewport.getViewportOptions().background;
|
||||||
|
|
||||||
|
// Todo: probably this can be done in a better way in which we identify bright
|
||||||
|
// background
|
||||||
|
const isLight = backgroundColor
|
||||||
|
? utilities.isEqual(backgroundColor, [1, 1, 1])
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewportOverlay
|
<ViewportOverlay
|
||||||
topLeft={getTopLeftContent()}
|
topLeft={getTopLeftContent()}
|
||||||
topRight={getTopRightContent()}
|
topRight={getTopRightContent()}
|
||||||
|
color={isLight && 'text-[#0944B3]'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||||
const imageIds = viewportData.imageIds;
|
const imageIds = viewportData.data.imageIds;
|
||||||
const imageId = imageIds[imageIndex];
|
const imageId = imageIds[imageIndex];
|
||||||
|
|
||||||
if (!imageId) {
|
if (!imageId) {
|
||||||
@ -258,8 +271,14 @@ function _getInstanceNumberFromVolume(
|
|||||||
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
const isAcquisitionPlane = vec3.length(cross) < EPSILON;
|
||||||
|
|
||||||
if (isAcquisitionPlane) {
|
if (isAcquisitionPlane) {
|
||||||
|
const imageId = imageIds[imageIndex];
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const { instanceNumber } =
|
const { instanceNumber } =
|
||||||
metaData.get('generalImageModule', imageIds[imageIndex]) || {};
|
metaData.get('generalImageModule', imageId) || {};
|
||||||
return parseInt(instanceNumber);
|
return parseInt(instanceNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
StackViewport,
|
StackViewport,
|
||||||
volumeLoader,
|
|
||||||
cache,
|
|
||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import {
|
import {
|
||||||
ToolGroupManager,
|
ToolGroupManager,
|
||||||
Enums,
|
Enums,
|
||||||
segmentation,
|
utilities as cstUtils,
|
||||||
utilities as csToolsUtils,
|
ReferenceLinesTool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
|
||||||
|
|
||||||
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
import { getEnabledElement as OHIFgetEnabledElement } from './state';
|
||||||
|
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
|
||||||
import callInputDialog from './utils/callInputDialog';
|
import callInputDialog from './utils/callInputDialog';
|
||||||
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
import { setColormap } from './utils/colormap/transferFunctionHelpers';
|
||||||
|
import toggleMPRHangingProtocol from './utils/mpr/toggleMPRHangingProtocol';
|
||||||
|
import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager }) => {
|
const commandsModule = ({ servicesManager }) => {
|
||||||
const {
|
const {
|
||||||
@ -25,7 +25,8 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
ToolBarService,
|
ToolBarService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
SegmentationService,
|
HangingProtocolService,
|
||||||
|
UINotificationService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
function _getActiveViewportEnabledElement() {
|
function _getActiveViewportEnabledElement() {
|
||||||
@ -128,76 +129,68 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
});
|
});
|
||||||
viewport.render();
|
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 }) => {
|
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||||
const toolGroup = _getToolGroup(toolGroupId);
|
if (toolName === 'Crosshairs') {
|
||||||
|
const activeViewportToolGroup = _getToolGroup(null);
|
||||||
|
|
||||||
if (!toolGroup) {
|
if (!activeViewportToolGroup._toolInstances.Crosshairs) {
|
||||||
console.warn('No tool group found for toolGroupId:', toolGroupId);
|
UINotificationService.show({
|
||||||
return;
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Todo: we need to check if the viewports of the toolGroup is actually
|
|
||||||
// parts of the ViewportGrid's viewports, if not we return
|
|
||||||
|
|
||||||
const { viewports } = ViewportGridService.getState() || {
|
const { viewports } = ViewportGridService.getState() || {
|
||||||
viewports: [],
|
viewports: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// iterate over all viewports and set the tool active for the
|
const toolGroup = _getToolGroup(toolGroupId);
|
||||||
// viewports that belong to the toolGroup
|
const toolGroupViewportIds = toolGroup.getViewportIds();
|
||||||
for (let index = 0; index < viewports.length; index++) {
|
|
||||||
const ohifEnabledElement = OHIFgetEnabledElement(index);
|
|
||||||
|
|
||||||
if (!ohifEnabledElement) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = getEnabledElement(ohifEnabledElement.element);
|
|
||||||
|
|
||||||
if (!viewport) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the current active tool and set it to be passive
|
|
||||||
const activeTool = toolGroup.getActivePrimaryMouseButtonTool();
|
|
||||||
|
|
||||||
if (activeTool) {
|
|
||||||
toolGroup.setToolPassive(activeTool);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the new toolName to be active
|
|
||||||
toolGroup.setToolActive(toolName, {
|
|
||||||
bindings: [{ mouseButton: Enums.MouseBindings.Primary }],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// if toolGroup has been destroyed, or its viewports have been removed
|
||||||
|
if (!toolGroupViewportIds || !toolGroupViewportIds.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredViewports = viewports.filter(viewport => {
|
||||||
|
if (!viewport.viewportOptions) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolGroupViewportIds.includes(
|
||||||
|
viewport.viewportOptions.viewportId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!filteredViewports.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeToolName = toolGroup.getActivePrimaryMouseButtonTool();
|
||||||
|
|
||||||
|
if (activeToolName) {
|
||||||
|
// Todo: this is a hack to prevent the crosshairs to stick around
|
||||||
|
// after another tool is selected. We should find a better way to do this
|
||||||
|
if (activeToolName === 'Crosshairs') {
|
||||||
|
toolGroup.setToolDisabled(activeToolName);
|
||||||
|
} else {
|
||||||
|
toolGroup.setToolPassive(activeToolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set the new toolName to be active
|
||||||
|
toolGroup.setToolActive(toolName, {
|
||||||
|
bindings: [
|
||||||
|
{
|
||||||
|
mouseButton: Enums.MouseBindings.Primary,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
showDownloadViewportModal: () => {
|
showDownloadViewportModal: () => {
|
||||||
const { activeViewportIndex } = ViewportGridService.getState();
|
const { activeViewportIndex } = ViewportGridService.getState();
|
||||||
@ -293,8 +286,12 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof StackViewport) {
|
||||||
viewport.resetProperties();
|
viewport.resetProperties();
|
||||||
viewport.resetCamera();
|
viewport.resetCamera();
|
||||||
viewport.render();
|
} else {
|
||||||
|
// Todo: add reset properties for volume viewport
|
||||||
|
viewport.resetCamera();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewport.render();
|
||||||
},
|
},
|
||||||
scaleViewport: ({ direction }) => {
|
scaleViewport: ({ direction }) => {
|
||||||
const enabledElement = _getActiveViewportEnabledElement();
|
const enabledElement = _getActiveViewportEnabledElement();
|
||||||
@ -326,59 +323,7 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
const { viewport } = enabledElement;
|
const { viewport } = enabledElement;
|
||||||
const options = { delta: direction };
|
const options = { delta: direction };
|
||||||
|
|
||||||
csToolsUtils.scroll(viewport, options);
|
cstUtils.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;
|
|
||||||
},
|
},
|
||||||
setViewportColormap: ({
|
setViewportColormap: ({
|
||||||
viewportIndex,
|
viewportIndex,
|
||||||
@ -393,7 +338,7 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
const actorEntries = viewport.getActors();
|
const actorEntries = viewport.getActors();
|
||||||
|
|
||||||
const actorEntry = actorEntries.find(actorEntry => {
|
const actorEntry = actorEntries.find(actorEntry => {
|
||||||
return actorEntry.uid === displaySetInstanceUID;
|
return actorEntry.uid.includes(displaySetInstanceUID);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { actor: volumeActor } = actorEntry;
|
const { actor: volumeActor } = actorEntry;
|
||||||
@ -404,6 +349,56 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
viewport.render();
|
viewport.render();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
incrementActiveViewport: () => {
|
||||||
|
const { activeViewportIndex, viewports } = ViewportGridService.getState();
|
||||||
|
const nextViewportIndex = (activeViewportIndex + 1) % viewports.length;
|
||||||
|
ViewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||||
|
},
|
||||||
|
decrementActiveViewport: () => {
|
||||||
|
const { activeViewportIndex, viewports } = ViewportGridService.getState();
|
||||||
|
const nextViewportIndex =
|
||||||
|
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
||||||
|
ViewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||||
|
},
|
||||||
|
setHangingProtocol: ({ protocolId }) => {
|
||||||
|
HangingProtocolService.setProtocol(protocolId);
|
||||||
|
},
|
||||||
|
toggleMPR: ({ toggledState }) => {
|
||||||
|
toggleMPRHangingProtocol({
|
||||||
|
toggledState,
|
||||||
|
servicesManager,
|
||||||
|
getToolGroup: _getToolGroup,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
toggleStackImageSync: ({ toggledState }) => {
|
||||||
|
toggleStackImageSync({
|
||||||
|
getEnabledElement,
|
||||||
|
servicesManager,
|
||||||
|
toggledState,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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 = {
|
const definitions = {
|
||||||
@ -417,11 +412,6 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
toggleCrosshairs: {
|
|
||||||
commandFn: actions.toggleCrosshairs,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
rotateViewportCW: {
|
rotateViewportCW: {
|
||||||
commandFn: actions.rotateViewport,
|
commandFn: actions.rotateViewport,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
@ -432,6 +422,14 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { rotation: -90 },
|
options: { rotation: -90 },
|
||||||
},
|
},
|
||||||
|
incrementActiveViewport: {
|
||||||
|
commandFn: actions.incrementActiveViewport,
|
||||||
|
storeContexts: [],
|
||||||
|
},
|
||||||
|
decrementActiveViewport: {
|
||||||
|
commandFn: actions.decrementActiveViewport,
|
||||||
|
storeContexts: [],
|
||||||
|
},
|
||||||
flipViewportHorizontal: {
|
flipViewportHorizontal: {
|
||||||
commandFn: actions.flipViewportHorizontal,
|
commandFn: actions.flipViewportHorizontal,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
@ -497,27 +495,31 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
createSegmentationForDisplaySet: {
|
|
||||||
commandFn: actions.createSegmentationForDisplaySet,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
addSegmentationRepresentationToToolGroup: {
|
|
||||||
commandFn: actions.addSegmentationRepresentationToToolGroup,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
|
|
||||||
getLabelmapVolumes: {
|
|
||||||
commandFn: actions.getLabelmapVolumes,
|
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
setViewportColormap: {
|
setViewportColormap: {
|
||||||
commandFn: actions.setViewportColormap,
|
commandFn: actions.setViewportColormap,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
setHangingProtocol: {
|
||||||
|
commandFn: actions.setHangingProtocol,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
toggleMPR: {
|
||||||
|
commandFn: actions.toggleMPR,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
toggleStackImageSync: {
|
||||||
|
commandFn: actions.toggleStackImageSync,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
toggleReferenceLines: {
|
||||||
|
commandFn: actions.toggleReferenceLines,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
142
extensions/cornerstone/src/getHangingProtocolModule.ts
Normal file
142
extensions/cornerstone/src/getHangingProtocolModule.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportOptions: {
|
||||||
|
toolGroupId: 'mpr',
|
||||||
|
viewportType: 'volume',
|
||||||
|
orientation: 'axial',
|
||||||
|
initialImageOptions: {
|
||||||
|
preset: 'middle',
|
||||||
|
},
|
||||||
|
syncGroups: [
|
||||||
|
{
|
||||||
|
type: 'voi',
|
||||||
|
id: 'mpr',
|
||||||
|
source: true,
|
||||||
|
target: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displaySets: [
|
||||||
|
{
|
||||||
|
id: 'mprDisplaySet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: mpr,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getHangingProtocolModule;
|
||||||
@ -3,22 +3,26 @@ import * as cornerstone from '@cornerstonejs/core';
|
|||||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||||
import {
|
import {
|
||||||
Enums as cs3DEnums,
|
Enums as cs3DEnums,
|
||||||
CONSTANTS,
|
|
||||||
imageLoadPoolManager,
|
imageLoadPoolManager,
|
||||||
imageRetrievalPoolManager,
|
imageRetrievalPoolManager,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools';
|
||||||
import init from './init.js';
|
import init from './init';
|
||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import ToolGroupService from './services/ToolGroupService';
|
import ToolGroupService from './services/ToolGroupService';
|
||||||
import SyncGroupService from './services/SyncGroupService';
|
import SyncGroupService from './services/SyncGroupService';
|
||||||
|
import SegmentationService from './services/SegmentationService';
|
||||||
|
import CornerstoneCacheService from './services/CornerstoneCacheService';
|
||||||
|
|
||||||
import { toolNames } from './initCornerstoneTools';
|
import { toolNames } from './initCornerstoneTools';
|
||||||
import { getEnabledElement } from './state';
|
import { getEnabledElement, reset as enabledElementReset } from './state';
|
||||||
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
import CornerstoneViewportService from './services/ViewportService/CornerstoneViewportService';
|
||||||
import dicomLoaderService from './utils/dicomLoaderService';
|
import dicomLoaderService from './utils/dicomLoaderService';
|
||||||
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||||
|
|
||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
|
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import(
|
return import(
|
||||||
@ -50,6 +54,9 @@ const cornerstoneExtension = {
|
|||||||
imageLoadPoolManager.clearRequestStack(type);
|
imageLoadPoolManager.clearRequestStack(type);
|
||||||
imageRetrievalPoolManager.clearRequestStack(type);
|
imageRetrievalPoolManager.clearRequestStack(type);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
csWADOImageLoader.destroy();
|
||||||
|
enabledElementReset();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,8 +76,12 @@ const cornerstoneExtension = {
|
|||||||
);
|
);
|
||||||
servicesManager.registerService(ToolGroupService(servicesManager));
|
servicesManager.registerService(ToolGroupService(servicesManager));
|
||||||
servicesManager.registerService(SyncGroupService(servicesManager));
|
servicesManager.registerService(SyncGroupService(servicesManager));
|
||||||
|
servicesManager.registerService(SegmentationService(servicesManager));
|
||||||
|
servicesManager.registerService(CornerstoneCacheService(servicesManager));
|
||||||
|
|
||||||
await init({ servicesManager, commandsManager, configuration, appConfig });
|
await init({ servicesManager, commandsManager, configuration, appConfig });
|
||||||
},
|
},
|
||||||
|
getHangingProtocolModule,
|
||||||
getViewportModule({ servicesManager, commandsManager }) {
|
getViewportModule({ servicesManager, commandsManager }) {
|
||||||
const ExtendedOHIFCornerstoneViewport = props => {
|
const ExtendedOHIFCornerstoneViewport = props => {
|
||||||
// const onNewImageHandler = jumpData => {
|
// const onNewImageHandler = jumpData => {
|
||||||
@ -119,7 +130,6 @@ const cornerstoneExtension = {
|
|||||||
name: 'core',
|
name: 'core',
|
||||||
exports: {
|
exports: {
|
||||||
Enums: cs3DEnums,
|
Enums: cs3DEnums,
|
||||||
CONSTANTS,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
|
import React from 'react';
|
||||||
import { ContextMenuMeasurements } from '@ohif/ui';
|
import { ContextMenuMeasurements } from '@ohif/ui';
|
||||||
|
|
||||||
import * as cornerstone from '@cornerstonejs/core';
|
import * as cornerstone from '@cornerstonejs/core';
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
imageLoadPoolManager,
|
imageLoadPoolManager,
|
||||||
Settings,
|
Settings,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { Enums, utilities } from '@cornerstonejs/tools';
|
import { Enums, utilities, ReferenceLinesTool } from '@cornerstonejs/tools';
|
||||||
import {
|
import {
|
||||||
cornerstoneStreamingImageVolumeLoader,
|
cornerstoneStreamingImageVolumeLoader,
|
||||||
sharedArrayBufferImageLoader,
|
sharedArrayBufferImageLoader,
|
||||||
@ -27,7 +28,6 @@ import callInputDialog from './utils/callInputDialog';
|
|||||||
import initCineService from './initCineService';
|
import initCineService from './initCineService';
|
||||||
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
import interleaveCenterLoader from './utils/interleaveCenterLoader';
|
||||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||||
import initSegmentationService from './initSegmentationService';
|
|
||||||
|
|
||||||
const cs3DToolsEvents = Enums.Events;
|
const cs3DToolsEvents = Enums.Events;
|
||||||
|
|
||||||
@ -48,34 +48,56 @@ export default async function init({
|
|||||||
await cs3DInit();
|
await cs3DInit();
|
||||||
|
|
||||||
// For debugging e2e tests that are failing on CI
|
// For debugging e2e tests that are failing on CI
|
||||||
//cornerstone.setUseCPURendering(true);
|
cornerstone.setUseCPURendering(Boolean(appConfig.useCPURendering));
|
||||||
|
|
||||||
// For debugging large datasets
|
// For debugging large datasets
|
||||||
//cornerstone.cache.setMaxCacheSize(3000000000);
|
const MAX_CACHE_SIZE_1GB = 1073741824;
|
||||||
|
const maxCacheSize = appConfig.maxCacheSize;
|
||||||
|
cornerstone.cache.setMaxCacheSize(
|
||||||
|
maxCacheSize ? maxCacheSize : MAX_CACHE_SIZE_1GB
|
||||||
|
);
|
||||||
|
|
||||||
initCornerstoneTools();
|
initCornerstoneTools();
|
||||||
|
|
||||||
// Don't use cursors in viewports
|
Settings.getRuntimeSettings().set(
|
||||||
// Todo: this should come from extension/app configuration
|
'useCursors',
|
||||||
Settings.getRuntimeSettings().set('useCursors', false);
|
Boolean(appConfig.useCursors)
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
UserAuthenticationService,
|
UserAuthenticationService,
|
||||||
ToolGroupService,
|
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
UIModalService,
|
||||||
CineService,
|
CineService,
|
||||||
CornerstoneViewportService,
|
CornerstoneViewportService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
SegmentationService,
|
ToolGroupService,
|
||||||
|
ViewportGridService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
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;
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||||
|
|
||||||
volumeLoader.registerUnknownVolumeLoader(
|
|
||||||
cornerstoneStreamingImageVolumeLoader
|
|
||||||
);
|
|
||||||
volumeLoader.registerVolumeLoader(
|
volumeLoader.registerVolumeLoader(
|
||||||
'cornerstoneStreamingImageVolume',
|
'cornerstoneStreamingImageVolume',
|
||||||
cornerstoneStreamingImageVolumeLoader
|
cornerstoneStreamingImageVolumeLoader
|
||||||
@ -112,8 +134,6 @@ export default async function init({
|
|||||||
CornerstoneViewportService
|
CornerstoneViewportService
|
||||||
);
|
);
|
||||||
|
|
||||||
initSegmentationService(SegmentationService, CornerstoneViewportService);
|
|
||||||
|
|
||||||
initCineService(CineService);
|
initCineService(CineService);
|
||||||
|
|
||||||
const _getDefaultPosition = event => ({
|
const _getDefaultPosition = event => ({
|
||||||
@ -135,8 +155,8 @@ export default async function init({
|
|||||||
currentPoints.canvas
|
currentPoints.canvas
|
||||||
);
|
);
|
||||||
|
|
||||||
let menuItems = [];
|
const menuItems = [];
|
||||||
if (nearbyToolData) {
|
if (nearbyToolData && nearbyToolData.metadata.toolName !== 'Crosshairs') {
|
||||||
defaultMenuItems.forEach(item => {
|
defaultMenuItems.forEach(item => {
|
||||||
item.value = nearbyToolData;
|
item.value = nearbyToolData;
|
||||||
item.element = element;
|
item.element = element;
|
||||||
@ -259,6 +279,37 @@ export default async function init({
|
|||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
const newStackCallback = evt => {
|
||||||
|
const { element } = evt.detail;
|
||||||
|
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) {
|
function elementEnabledHandler(evt) {
|
||||||
const { element } = evt.detail;
|
const { element } = evt.detail;
|
||||||
|
|
||||||
@ -267,27 +318,29 @@ export default async function init({
|
|||||||
contextMenuHandleClick
|
contextMenuHandleClick
|
||||||
);
|
);
|
||||||
|
|
||||||
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||||
const { element } = evt.detail;
|
|
||||||
utilities.stackPrefetch.enable(element);
|
eventTarget.addEventListener(
|
||||||
});
|
EVENTS.STACK_VIEWPORT_NEW_STACK,
|
||||||
|
newStackCallback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementDisabledHandler(evt) {
|
function elementDisabledHandler(evt) {
|
||||||
const { viewportId, element } = evt.detail;
|
const { element } = evt.detail;
|
||||||
|
|
||||||
const viewportInfo = CornerstoneViewportService.getViewportInfo(viewportId);
|
|
||||||
ToolGroupService.disable(viewportInfo);
|
|
||||||
|
|
||||||
element.removeEventListener(
|
element.removeEventListener(
|
||||||
cs3DToolsEvents.MOUSE_CLICK,
|
cs3DToolsEvents.MOUSE_CLICK,
|
||||||
contextMenuHandleClick
|
contextMenuHandleClick
|
||||||
);
|
);
|
||||||
|
|
||||||
eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
|
element.removeEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||||
const { element } = evt.detail;
|
|
||||||
utilities.stackPrefetch.disable(element);
|
// TODO - consider removing the callback when all elements are gone
|
||||||
});
|
// eventTarget.removeEventListener(
|
||||||
|
// EVENTS.STACK_VIEWPORT_NEW_STACK,
|
||||||
|
// newStackCallback
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
eventTarget.addEventListener(
|
eventTarget.addEventListener(
|
||||||
@ -299,4 +352,74 @@ export default async function init({
|
|||||||
EVENTS.ELEMENT_DISABLED,
|
EVENTS.ELEMENT_DISABLED,
|
||||||
elementDisabledHandler.bind(null)
|
elementDisabledHandler.bind(null)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ViewportGridService.subscribe(
|
||||||
|
ViewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
|
||||||
|
({ viewportIndex }) => {
|
||||||
|
const viewportId = `viewport-${viewportIndex}`;
|
||||||
|
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
|
||||||
|
|
||||||
|
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
|
||||||
|
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,
|
BidirectionalTool,
|
||||||
ArrowAnnotateTool,
|
ArrowAnnotateTool,
|
||||||
DragProbeTool,
|
DragProbeTool,
|
||||||
|
ProbeTool,
|
||||||
AngleTool,
|
AngleTool,
|
||||||
MagnifyTool,
|
MagnifyTool,
|
||||||
CrosshairsTool,
|
CrosshairsTool,
|
||||||
@ -19,6 +20,7 @@ import {
|
|||||||
init,
|
init,
|
||||||
addTool,
|
addTool,
|
||||||
annotation,
|
annotation,
|
||||||
|
ReferenceLinesTool,
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
export default function initCornerstoneTools(configuration = {}) {
|
export default function initCornerstoneTools(configuration = {}) {
|
||||||
@ -28,6 +30,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(StackScrollMouseWheelTool);
|
addTool(StackScrollMouseWheelTool);
|
||||||
addTool(StackScrollTool);
|
addTool(StackScrollTool);
|
||||||
addTool(ZoomTool);
|
addTool(ZoomTool);
|
||||||
|
addTool(ProbeTool);
|
||||||
addTool(VolumeRotateMouseWheelTool);
|
addTool(VolumeRotateMouseWheelTool);
|
||||||
addTool(MIPJumpToClickTool);
|
addTool(MIPJumpToClickTool);
|
||||||
addTool(LengthTool);
|
addTool(LengthTool);
|
||||||
@ -40,6 +43,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
|||||||
addTool(MagnifyTool);
|
addTool(MagnifyTool);
|
||||||
addTool(CrosshairsTool);
|
addTool(CrosshairsTool);
|
||||||
addTool(SegmentationDisplayTool);
|
addTool(SegmentationDisplayTool);
|
||||||
|
addTool(ReferenceLinesTool);
|
||||||
|
|
||||||
// Modify annotation tools to use dashed lines on SR
|
// Modify annotation tools to use dashed lines on SR
|
||||||
const annotationStyle = {
|
const annotationStyle = {
|
||||||
@ -67,6 +71,7 @@ const toolNames = {
|
|||||||
MipJumpToClick: MIPJumpToClickTool.toolName,
|
MipJumpToClick: MIPJumpToClickTool.toolName,
|
||||||
Length: LengthTool.toolName,
|
Length: LengthTool.toolName,
|
||||||
DragProbe: DragProbeTool.toolName,
|
DragProbe: DragProbeTool.toolName,
|
||||||
|
Probe: ProbeTool.toolName,
|
||||||
RectangleROI: RectangleROITool.toolName,
|
RectangleROI: RectangleROITool.toolName,
|
||||||
EllipticalROI: EllipticalROITool.toolName,
|
EllipticalROI: EllipticalROITool.toolName,
|
||||||
Bidirectional: BidirectionalTool.toolName,
|
Bidirectional: BidirectionalTool.toolName,
|
||||||
@ -74,6 +79,7 @@ const toolNames = {
|
|||||||
Magnify: MagnifyTool.toolName,
|
Magnify: MagnifyTool.toolName,
|
||||||
Crosshairs: CrosshairsTool.toolName,
|
Crosshairs: CrosshairsTool.toolName,
|
||||||
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
SegmentationDisplay: SegmentationDisplayTool.toolName,
|
||||||
|
ReferenceLines: ReferenceLinesTool.toolName,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { toolNames };
|
export { toolNames };
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Enums, annotation } from '@cornerstonejs/tools';
|
|||||||
import { DicomMetadataStore } from '@ohif/core';
|
import { DicomMetadataStore } from '@ohif/core';
|
||||||
|
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
|
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||||
|
|
||||||
const { removeAnnotation } = annotation.state;
|
const { removeAnnotation } = annotation.state;
|
||||||
|
|
||||||
@ -252,7 +253,18 @@ const connectMeasurementServiceToTools = (
|
|||||||
SOPInstanceUID
|
SOPInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageId = dataSource.getImageIdsForInstance({ instance });
|
let imageId;
|
||||||
|
let frameNumber = 1;
|
||||||
|
|
||||||
|
if (measurement?.metadata?.referencedImageId) {
|
||||||
|
imageId = measurement.metadata.referencedImageId;
|
||||||
|
frameNumber = getSOPInstanceAttributes(
|
||||||
|
measurement.metadata.referencedImageId
|
||||||
|
).frameNumber;
|
||||||
|
} else {
|
||||||
|
imageId = dataSource.getImageIdsForInstance({ instance });
|
||||||
|
}
|
||||||
|
|
||||||
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
const annotationManager = annotation.state.getDefaultAnnotationManager();
|
||||||
annotationManager.addAnnotation({
|
annotationManager.addAnnotation({
|
||||||
annotationUID: measurement.uid,
|
annotationUID: measurement.uid,
|
||||||
@ -269,6 +281,7 @@ const connectMeasurementServiceToTools = (
|
|||||||
handles: { ...data.annotation.data.handles },
|
handles: { ...data.annotation.data.handles },
|
||||||
cachedStats: { ...data.annotation.data.cachedStats },
|
cachedStats: { ...data.annotation.data.cachedStats },
|
||||||
label: data.annotation.data.label,
|
label: data.annotation.data.label,
|
||||||
|
frameNumber: frameNumber,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 * as cornerstone from '@cornerstonejs/core';
|
||||||
import { volumeLoader } from '@cornerstonejs/core';
|
import { volumeLoader } from '@cornerstonejs/core';
|
||||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
import { cornerstoneStreamingImageVolumeLoader } 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 dicomParser from 'dicom-parser';
|
||||||
import { errorHandler } from '@ohif/core';
|
import { errorHandler } from '@ohif/core';
|
||||||
|
|
||||||
@ -61,7 +63,9 @@ export default function initWADOImageLoader(
|
|||||||
// For now we use image/jls and image/x-jls because some servers still use the old type
|
// For now we use image/jls and image/x-jls because some servers still use the old type
|
||||||
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
|
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
|
||||||
const xhrRequestHeaders = {
|
const xhrRequestHeaders = {
|
||||||
Accept: 'multipart/related; type=application/octet-stream',
|
Accept: appConfig.omitQuotationForMultipartRequest
|
||||||
|
? 'multipart/related; type=application/octet-stream'
|
||||||
|
: 'multipart/related; type="application/octet-stream"',
|
||||||
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
|
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -78,3 +82,13 @@ export default function initWADOImageLoader(
|
|||||||
|
|
||||||
initWebWorkers(appConfig);
|
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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,4 +1,8 @@
|
|||||||
import { synchronizers, SynchronizerManager } from '@cornerstonejs/tools';
|
import {
|
||||||
|
synchronizers,
|
||||||
|
SynchronizerManager,
|
||||||
|
Synchronizer,
|
||||||
|
} from '@cornerstonejs/tools';
|
||||||
|
|
||||||
import { pubSubServiceInterface } from '@ohif/core';
|
import { pubSubServiceInterface } from '@ohif/core';
|
||||||
|
|
||||||
@ -6,20 +10,42 @@ const EVENTS = {
|
|||||||
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @params options - are an optional set of options associated with the first
|
||||||
|
* sync group declared.
|
||||||
|
*/
|
||||||
|
export type SyncCreator = (
|
||||||
|
type: string,
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
) => Synchronizer;
|
||||||
|
|
||||||
export type SyncGroup = {
|
export type SyncGroup = {
|
||||||
type: string;
|
type: string;
|
||||||
id: string;
|
id?: string;
|
||||||
source: boolean;
|
// Source and target default to true if not specified
|
||||||
target: boolean;
|
source?: boolean;
|
||||||
|
target?: boolean;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const POSITION = 'cameraposition';
|
const POSITION = 'cameraposition';
|
||||||
const VOI = 'voi';
|
const VOI = 'voi';
|
||||||
|
const ZOOMPAN = 'zoompan';
|
||||||
|
const STACKIMAGE = 'stackimage';
|
||||||
|
|
||||||
|
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
|
||||||
|
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
|
||||||
|
|
||||||
export default class SyncGroupService {
|
export default class SyncGroupService {
|
||||||
serviceManager: any;
|
serviceManager: any;
|
||||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
listeners: { [key: string]: (...args: any[]) => void } = {};
|
||||||
EVENTS: { [key: string]: string };
|
EVENTS: { [key: string]: string };
|
||||||
|
synchronizerCreators: Record<string, SyncCreator> = {
|
||||||
|
[POSITION]: synchronizers.createCameraPositionSynchronizer,
|
||||||
|
[VOI]: synchronizers.createVOISynchronizer,
|
||||||
|
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
|
||||||
|
[STACKIMAGE]: synchronizers.createStackImageSynchronizer,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(serviceManager) {
|
constructor(serviceManager) {
|
||||||
this.serviceManager = serviceManager;
|
this.serviceManager = serviceManager;
|
||||||
@ -29,64 +55,95 @@ export default class SyncGroupService {
|
|||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createSynchronizer(type: string, id: string) {
|
private _createSynchronizer(
|
||||||
type = type.toLowerCase();
|
type: string,
|
||||||
if (type === POSITION) {
|
id: string,
|
||||||
return synchronizers.createCameraPositionSynchronizer(id);
|
options
|
||||||
} else if (type === VOI) {
|
): Synchronizer | undefined {
|
||||||
return synchronizers.createVOISynchronizer(id);
|
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
|
||||||
|
if (syncCreator) {
|
||||||
|
return syncCreator(id, options);
|
||||||
|
} else {
|
||||||
|
console.warn('Unknown synchronizer type', type, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a synchronizer type.
|
||||||
|
* @param type is the type of the synchronizer to create
|
||||||
|
* @param creator
|
||||||
|
*/
|
||||||
|
public setSynchronizer(type: string, creator: SyncCreator): void {
|
||||||
|
this.synchronizerCreators[type.toLowerCase()] = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getOrCreateSynchronizer(
|
||||||
|
type: string,
|
||||||
|
id: string,
|
||||||
|
options: Record<string, unknown>
|
||||||
|
): Synchronizer | undefined {
|
||||||
|
let synchronizer = SynchronizerManager.getSynchronizer(id);
|
||||||
|
|
||||||
|
if (!synchronizer) {
|
||||||
|
synchronizer = this._createSynchronizer(type, id, options);
|
||||||
|
}
|
||||||
|
return synchronizer;
|
||||||
|
}
|
||||||
|
|
||||||
public addViewportToSyncGroup(
|
public addViewportToSyncGroup(
|
||||||
viewportId: string,
|
viewportId: string,
|
||||||
renderingEngineId: string,
|
renderingEngineId: string,
|
||||||
syncGroups?: SyncGroup[]
|
syncGroups?: SyncGroup | string | SyncGroup[] | string[]
|
||||||
): void {
|
): void {
|
||||||
if (!syncGroups || !syncGroups.length) {
|
if (!syncGroups) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
syncGroups.forEach(syncGroup => {
|
const syncGroupsArray = Array.isArray(syncGroups)
|
||||||
const { type, id, target, source } = syncGroup;
|
? syncGroups
|
||||||
|
: [syncGroups];
|
||||||
|
|
||||||
let synchronizer = SynchronizerManager.getSynchronizer(id);
|
syncGroupsArray.forEach(syncGroup => {
|
||||||
|
const syncGroupObj = asSyncGroup(syncGroup);
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
target = true,
|
||||||
|
source = true,
|
||||||
|
options = {},
|
||||||
|
id = type,
|
||||||
|
} = syncGroupObj;
|
||||||
|
|
||||||
if (!synchronizer) {
|
const synchronizer = this._getOrCreateSynchronizer(type, id, options);
|
||||||
synchronizer = this._createSynchronizer(type, id);
|
synchronizer.setOptions(viewportId, options);
|
||||||
}
|
|
||||||
|
|
||||||
|
const viewportInfo = { viewportId, renderingEngineId };
|
||||||
if (target && source) {
|
if (target && source) {
|
||||||
synchronizer.add({
|
synchronizer.add(viewportInfo);
|
||||||
viewportId,
|
|
||||||
renderingEngineId,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
} else if (source) {
|
} else if (source) {
|
||||||
synchronizer.addSource({
|
synchronizer.addSource(viewportInfo);
|
||||||
viewportId,
|
|
||||||
renderingEngineId,
|
|
||||||
});
|
|
||||||
} else if (target) {
|
} else if (target) {
|
||||||
synchronizer.addTarget({
|
synchronizer.addTarget(viewportInfo);
|
||||||
viewportId,
|
|
||||||
renderingEngineId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy(): void {
|
||||||
SynchronizerManager.destroy();
|
SynchronizerManager.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeViewportFromSyncGroup(
|
public removeViewportFromSyncGroup(
|
||||||
viewportId: string,
|
viewportId: string,
|
||||||
renderingEngineId: string
|
renderingEngineId: string,
|
||||||
|
syncGroupId?: string
|
||||||
): void {
|
): void {
|
||||||
const synchronizers = SynchronizerManager.getAllSynchronizers();
|
const synchronizers = SynchronizerManager.getAllSynchronizers();
|
||||||
|
|
||||||
synchronizers.forEach(synchronizer => {
|
const filteredSynchronizers = syncGroupId
|
||||||
|
? synchronizers.filter(s => s.id === syncGroupId)
|
||||||
|
: synchronizers;
|
||||||
|
|
||||||
|
filteredSynchronizers.forEach(synchronizer => {
|
||||||
if (!synchronizer) {
|
if (!synchronizer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,16 @@ export default class ToolGroupService {
|
|||||||
this.toolGroupIds = new Set();
|
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(
|
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||||
viewportId,
|
viewportId,
|
||||||
renderingEngineId
|
renderingEngineId
|
||||||
@ -85,9 +94,10 @@ export default class ToolGroupService {
|
|||||||
toolGroup.removeViewports(renderingEngineId, viewportId);
|
toolGroup.removeViewports(renderingEngineId, viewportId);
|
||||||
|
|
||||||
const viewportIds = toolGroup.getViewportIds();
|
const viewportIds = toolGroup.getViewportIds();
|
||||||
// if (viewportIds.length === 0) {
|
|
||||||
// ToolGroupManager.destroyToolGroup(toolGroup.id);
|
if (viewportIds.length === 0 && deleteToolGroupIfEmpty) {
|
||||||
// }
|
ToolGroupManager.destroyToolGroup(toolGroup.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public addViewportToToolGroup(
|
public addViewportToToolGroup(
|
||||||
@ -110,7 +120,10 @@ export default class ToolGroupService {
|
|||||||
toolGroup.addViewport(viewportId, renderingEngineId);
|
toolGroup.addViewport(viewportId, renderingEngineId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, { viewportId, toolGroupId });
|
this._broadcastEvent(EVENTS.VIEWPORT_ADDED, {
|
||||||
|
viewportId,
|
||||||
|
toolGroupId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public createToolGroup(toolGroupId: string): Types.IToolGroup {
|
public createToolGroup(toolGroupId: string): Types.IToolGroup {
|
||||||
@ -122,7 +135,9 @@ export default class ToolGroupService {
|
|||||||
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
|
const toolGroup = ToolGroupManager.createToolGroup(toolGroupId);
|
||||||
this.toolGroupIds.add(toolGroupId);
|
this.toolGroupIds.add(toolGroupId);
|
||||||
|
|
||||||
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, { toolGroupId });
|
this._broadcastEvent(EVENTS.TOOLGROUP_CREATED, {
|
||||||
|
toolGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
return toolGroup;
|
return toolGroup;
|
||||||
}
|
}
|
||||||
@ -195,9 +210,11 @@ export default class ToolGroupService {
|
|||||||
|
|
||||||
private _getToolNames(toolGroupTools: Tools): string[] {
|
private _getToolNames(toolGroupTools: Tools): string[] {
|
||||||
const toolNames = [];
|
const toolNames = [];
|
||||||
toolGroupTools.active.forEach(tool => {
|
if (toolGroupTools.active) {
|
||||||
toolNames.push(tool.toolName);
|
toolGroupTools.active.forEach(tool => {
|
||||||
});
|
toolNames.push(tool.toolName);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (toolGroupTools.passive) {
|
if (toolGroupTools.passive) {
|
||||||
toolGroupTools.passive.forEach(tool => {
|
toolGroupTools.passive.forEach(tool => {
|
||||||
toolNames.push(tool.toolName);
|
toolNames.push(tool.toolName);
|
||||||
@ -221,9 +238,12 @@ export default class ToolGroupService {
|
|||||||
|
|
||||||
private _setToolsMode(toolGroup, tools) {
|
private _setToolsMode(toolGroup, tools) {
|
||||||
const { active, passive, enabled, disabled } = 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) {
|
if (passive) {
|
||||||
passive.forEach(({ toolName }) => {
|
passive.forEach(({ toolName }) => {
|
||||||
|
|||||||
@ -1,227 +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',
|
|
||||||
};
|
|
||||||
|
|
||||||
class CornerstoneCacheService {
|
|
||||||
stackImageIds: Map<string, string[]> = new Map();
|
|
||||||
volumeImageIds: Map<string, string[]> = new Map();
|
|
||||||
listeners: { [key: string]: (...args: any[]) => void } = {};
|
|
||||||
EVENTS: { [key: string]: string };
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.listeners = {};
|
|
||||||
this.EVENTS = EVENTS;
|
|
||||||
Object.assign(this, pubSubServiceInterface);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCacheSize() {
|
|
||||||
return cs3DCache.getCacheSize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCacheFreeSpace() {
|
|
||||||
return cs3DCache.getBytesAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getViewportData(
|
|
||||||
viewportIndex: number,
|
|
||||||
displaySets: unknown[],
|
|
||||||
viewportType: string,
|
|
||||||
dataSource: unknown,
|
|
||||||
initialImageIndex?: number
|
|
||||||
): Promise<StackData | VolumeData> {
|
|
||||||
const cs3DViewportType = getCornerstoneViewportType(viewportType);
|
|
||||||
let viewportData: StackData | VolumeData;
|
|
||||||
|
|
||||||
if (cs3DViewportType === Enums.ViewportType.STACK) {
|
|
||||||
viewportData = await this._getStackViewportData(
|
|
||||||
dataSource,
|
|
||||||
displaySets,
|
|
||||||
initialImageIndex
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
|
||||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewportData.viewportType = cs3DViewportType;
|
|
||||||
|
|
||||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
|
||||||
viewportData,
|
|
||||||
viewportIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
return viewportData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async invalidateViewportData(
|
|
||||||
viewportData: VolumeData,
|
|
||||||
invalidatedDisplaySetInstanceUID: string,
|
|
||||||
dataSource,
|
|
||||||
DisplaySetService
|
|
||||||
) {
|
|
||||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
|
||||||
throw new Error('Invalidation of StackViewport is not supported yet');
|
|
||||||
}
|
|
||||||
|
|
||||||
const volumeId = invalidatedDisplaySetInstanceUID;
|
|
||||||
const volume = cs3DCache.getVolume(volumeId);
|
|
||||||
|
|
||||||
if (volume) {
|
|
||||||
cs3DCache.removeVolumeLoadObject(volumeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const displaySets = viewportData.displaySetInstanceUIDs.map(
|
|
||||||
DisplaySetService.getDisplaySetByUID
|
|
||||||
);
|
|
||||||
|
|
||||||
const newViewportData = await this._getVolumeViewportData(
|
|
||||||
dataSource,
|
|
||||||
displaySets
|
|
||||||
);
|
|
||||||
|
|
||||||
return newViewportData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getStackViewportData(
|
|
||||||
dataSource,
|
|
||||||
displaySets,
|
|
||||||
initialImageIndex
|
|
||||||
): StackData {
|
|
||||||
// For Stack Viewport we don't have fusion currently
|
|
||||||
const displaySet = displaySets[0];
|
|
||||||
|
|
||||||
let stackImageIds = this.stackImageIds.get(
|
|
||||||
displaySet.displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!stackImageIds) {
|
|
||||||
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
|
||||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
|
|
||||||
|
|
||||||
const stackData: StackData = {
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
viewportType: Enums.ViewportType.STACK,
|
|
||||||
imageIds: stackImageIds,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof initialImageIndex === 'number') {
|
|
||||||
stackData.initialImageIndex = initialImageIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return stackData;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _getVolumeViewportData(
|
|
||||||
dataSource,
|
|
||||||
displaySets
|
|
||||||
): Promise<VolumeData> {
|
|
||||||
// Check the cache for multiple scenarios to see if we need to
|
|
||||||
// decache the volume data from other viewports or not
|
|
||||||
|
|
||||||
const volumeImageIdsArray = [];
|
|
||||||
const volumes = [];
|
|
||||||
|
|
||||||
for (const displaySet of displaySets) {
|
|
||||||
const volumeId = displaySet.displaySetInstanceUID;
|
|
||||||
|
|
||||||
let volumeImageIds = this.volumeImageIds.get(
|
|
||||||
displaySet.displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
let volume = cs3DCache.getVolume(volumeId);
|
|
||||||
|
|
||||||
if (!volumeImageIds || !volume) {
|
|
||||||
volumeImageIds = this._getCornerstoneVolumeImageIds(
|
|
||||||
displaySet,
|
|
||||||
dataSource
|
|
||||||
);
|
|
||||||
|
|
||||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
|
||||||
imageIds: volumeImageIds,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.volumeImageIds.set(
|
|
||||||
displaySet.displaySetInstanceUID,
|
|
||||||
volumeImageIds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeImageIdsArray.push(volumeImageIds);
|
|
||||||
volumes.push(volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert displaySets are from the same study
|
|
||||||
const { StudyInstanceUID } = displaySets[0];
|
|
||||||
const displaySetInstanceUIDs = [];
|
|
||||||
|
|
||||||
displaySets.forEach(displaySet => {
|
|
||||||
if (displaySet.StudyInstanceUID !== StudyInstanceUID) {
|
|
||||||
throw new Error('Display sets are not from the same study');
|
|
||||||
}
|
|
||||||
|
|
||||||
displaySetInstanceUIDs.push(displaySet.displaySetInstanceUID);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUIDs,
|
|
||||||
imageIds: volumeImageIdsArray,
|
|
||||||
viewportType: Enums.ViewportType.ORTHOGRAPHIC,
|
|
||||||
volumes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getCornerstoneStackImageIds(displaySet, dataSource): string[] {
|
|
||||||
return dataSource.getImageIdsForDisplaySet(displaySet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
|
||||||
const stackImageIds = this._getCornerstoneStackImageIds(
|
|
||||||
displaySet,
|
|
||||||
dataSource
|
|
||||||
);
|
|
||||||
|
|
||||||
return stackImageIds.map(imageId => {
|
|
||||||
const imageURI = utils.imageIdToURI(imageId);
|
|
||||||
return `${VOLUME_LOADER_SCHEME}:${imageURI}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CacheService = new CornerstoneCacheService();
|
|
||||||
export default CacheService;
|
|
||||||
@ -2,7 +2,6 @@ import { pubSubServiceInterface } from '@ohif/core';
|
|||||||
import {
|
import {
|
||||||
RenderingEngine,
|
RenderingEngine,
|
||||||
StackViewport,
|
StackViewport,
|
||||||
Enums,
|
|
||||||
Types,
|
Types,
|
||||||
getRenderingEngine,
|
getRenderingEngine,
|
||||||
utilities as csUtils,
|
utilities as csUtils,
|
||||||
@ -18,7 +17,10 @@ import ViewportInfo, {
|
|||||||
DisplaySetOptions,
|
DisplaySetOptions,
|
||||||
PublicViewportOptions,
|
PublicViewportOptions,
|
||||||
} from './Viewport';
|
} from './Viewport';
|
||||||
import { StackData, VolumeData } from './CornerstoneCacheService';
|
import {
|
||||||
|
StackViewportData,
|
||||||
|
VolumeViewportData,
|
||||||
|
} from '../../types/CornerstoneCacheService';
|
||||||
import {
|
import {
|
||||||
setColormap,
|
setColormap,
|
||||||
setLowerUpperColorTransferFunction,
|
setLowerUpperColorTransferFunction,
|
||||||
@ -27,8 +29,8 @@ import {
|
|||||||
import JumpPresets from '../../utils/JumpPresets';
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
VIEWPORT_INFO_CREATED:
|
VIEWPORT_DATA_CHANGED:
|
||||||
'event::cornerstone::viewportservice:viewportinfocreated',
|
'event::cornerstoneViewportService:viewportDataChanged',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,6 +41,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
renderingEngine: Types.IRenderingEngine | null;
|
renderingEngine: Types.IRenderingEngine | null;
|
||||||
viewportsInfo: Map<number, ViewportInfo>;
|
viewportsInfo: Map<number, ViewportInfo>;
|
||||||
viewportGridResizeObserver: ResizeObserver | null;
|
viewportGridResizeObserver: ResizeObserver | null;
|
||||||
|
viewportsDisplaySets: Map<string, string[]> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service-specific
|
* Service-specific
|
||||||
@ -50,6 +53,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
enableResizeDetector: true;
|
enableResizeDetector: true;
|
||||||
resizeRefreshRateMs: 200;
|
resizeRefreshRateMs: 200;
|
||||||
resizeRefreshMode: 'debounce';
|
resizeRefreshMode: 'debounce';
|
||||||
|
servicesManager = null;
|
||||||
|
|
||||||
constructor(servicesManager) {
|
constructor(servicesManager) {
|
||||||
this.renderingEngine = null;
|
this.renderingEngine = null;
|
||||||
@ -58,8 +62,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
//
|
//
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
const { HangingProtocolService } = servicesManager.services;
|
this.servicesManager = servicesManager;
|
||||||
this.HangingProtocolService = HangingProtocolService;
|
|
||||||
Object.assign(this, pubSubServiceInterface);
|
Object.assign(this, pubSubServiceInterface);
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -69,18 +72,29 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
* @param {*} viewportIndex
|
* @param {*} viewportIndex
|
||||||
* @param {*} elementRef
|
* @param {*} elementRef
|
||||||
*/
|
*/
|
||||||
public enableElement(
|
public enableViewport(
|
||||||
viewportIndex: number,
|
viewportIndex: number,
|
||||||
viewportOptions: PublicViewportOptions,
|
viewportOptions: PublicViewportOptions,
|
||||||
elementRef: HTMLDivElement
|
elementRef: HTMLDivElement
|
||||||
) {
|
) {
|
||||||
const viewportId =
|
const viewportInfo = new ViewportInfo(
|
||||||
viewportOptions.viewportId || this.getViewportId(viewportIndex);
|
viewportIndex,
|
||||||
const viewportInfo = new ViewportInfo(viewportIndex, viewportId);
|
this.getViewportId(viewportIndex)
|
||||||
|
);
|
||||||
viewportInfo.setElement(elementRef);
|
viewportInfo.setElement(elementRef);
|
||||||
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
this.viewportsInfo.set(viewportIndex, viewportInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getViewportIds(): string[] {
|
||||||
|
const viewportIds = [];
|
||||||
|
|
||||||
|
this.viewportsInfo.forEach(viewportInfo => {
|
||||||
|
viewportIds.push(viewportInfo.getViewportId());
|
||||||
|
});
|
||||||
|
|
||||||
|
return viewportIds;
|
||||||
|
}
|
||||||
|
|
||||||
public getViewportId(viewportIndex: number): string {
|
public getViewportId(viewportIndex: number): string {
|
||||||
return `viewport-${viewportIndex}`;
|
return `viewport-${viewportIndex}`;
|
||||||
}
|
}
|
||||||
@ -110,10 +124,9 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
*/
|
*/
|
||||||
public resize() {
|
public resize() {
|
||||||
const immediate = true;
|
const immediate = true;
|
||||||
const resetPan = false;
|
const keepCamera = true;
|
||||||
const resetZoom = false;
|
|
||||||
|
|
||||||
this.renderingEngine.resize(immediate, resetPan, resetZoom);
|
this.renderingEngine.resize(immediate, keepCamera);
|
||||||
this.renderingEngine.render();
|
this.renderingEngine.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,8 +137,9 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
this._removeResizeObserver();
|
this._removeResizeObserver();
|
||||||
this.viewportGridResizeObserver = null;
|
this.viewportGridResizeObserver = null;
|
||||||
this.renderingEngine.destroy();
|
this.renderingEngine.destroy();
|
||||||
|
this.viewportsDisplaySets.clear();
|
||||||
this.renderingEngine = null;
|
this.renderingEngine = null;
|
||||||
cache.purgeVolumeCache();
|
cache.purgeCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -140,12 +154,11 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const viewportId = viewportInfo.getViewportId();
|
const viewportId = viewportInfo.getViewportId();
|
||||||
this.renderingEngine.disableElement(viewportId);
|
|
||||||
this.viewportsInfo.delete(viewportIndex);
|
|
||||||
|
|
||||||
if (this.viewportsInfo.size === 0) {
|
this.renderingEngine && this.renderingEngine.disableElement(viewportId);
|
||||||
this.destroy();
|
|
||||||
}
|
this.viewportsInfo.get(viewportIndex).destroy();
|
||||||
|
this.viewportsInfo.delete(viewportIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -156,29 +169,56 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
* @param {*} dataSource
|
* @param {*} dataSource
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public setViewportDisplaySets(
|
public setViewportData(
|
||||||
viewportIndex: number,
|
viewportIndex: number,
|
||||||
viewportData: StackData | VolumeData,
|
viewportData: StackViewportData | VolumeViewportData,
|
||||||
publicViewportOptions: PublicViewportOptions,
|
publicViewportOptions: PublicViewportOptions,
|
||||||
publicDisplaySetOptions: DisplaySetOptions[]
|
publicDisplaySetOptions: DisplaySetOptions[]
|
||||||
): void {
|
): void {
|
||||||
const renderingEngine = this.getRenderingEngine();
|
const renderingEngine = this.getRenderingEngine();
|
||||||
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
const viewportInfo = this.viewportsInfo.get(viewportIndex);
|
||||||
|
|
||||||
|
if (!publicViewportOptions.viewportId) {
|
||||||
|
publicViewportOptions.viewportId = this.getViewportId(viewportIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewportId = viewportInfo.getViewportId();
|
||||||
|
|
||||||
|
// if currently there is a viewport with the viewportId, but it is not the same
|
||||||
|
// as the one we are trying to set, we need to disable the old one
|
||||||
|
// and enable the new one, we could ideally change the name of the viewportId
|
||||||
|
// but the viewportId is an integral part in renderers map, tools svg cache
|
||||||
|
// etc. which would require a lot of refactoring, for now we will just disable
|
||||||
|
// the old one and enable the new one at the end of this function
|
||||||
|
let newViewportId = null;
|
||||||
|
if (publicViewportOptions?.viewportId !== viewportId) {
|
||||||
|
newViewportId = publicViewportOptions.viewportId;
|
||||||
|
viewportInfo.setViewportId(newViewportId);
|
||||||
|
|
||||||
|
renderingEngine.disableElement(viewportId);
|
||||||
|
}
|
||||||
|
|
||||||
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
viewportInfo.setRenderingEngineId(renderingEngine.id);
|
||||||
|
|
||||||
const { viewportOptions, displaySetOptions } =
|
const {
|
||||||
this._getViewportAndDisplaySetOptions(
|
viewportOptions,
|
||||||
publicViewportOptions,
|
displaySetOptions,
|
||||||
publicDisplaySetOptions,
|
} = this._getViewportAndDisplaySetOptions(
|
||||||
viewportInfo
|
publicViewportOptions,
|
||||||
);
|
publicDisplaySetOptions,
|
||||||
|
viewportInfo
|
||||||
|
);
|
||||||
|
|
||||||
viewportInfo.setViewportOptions(viewportOptions);
|
viewportInfo.setViewportOptions(viewportOptions);
|
||||||
viewportInfo.setDisplaySetOptions(displaySetOptions);
|
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();
|
viewportId = viewportInfo.getViewportId();
|
||||||
const element = viewportInfo.getElement();
|
const element = viewportInfo.getElement();
|
||||||
const type = viewportInfo.getViewportType();
|
const type = viewportInfo.getViewportType();
|
||||||
const background = viewportInfo.getBackground();
|
const background = viewportInfo.getBackground();
|
||||||
@ -199,7 +239,9 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
// renderingEngine is designed to be used like this. This will trigger
|
// renderingEngine is designed to be used like this. This will trigger
|
||||||
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
|
// ENABLED_ELEMENT again and again, which will run onEnableElement callbacks
|
||||||
renderingEngine.enableElement(viewportInput);
|
renderingEngine.enableElement(viewportInput);
|
||||||
this._setDisplaySets(viewportId, viewportData, viewportInfo);
|
|
||||||
|
const viewport = renderingEngine.getViewport(viewportId);
|
||||||
|
this._setDisplaySets(viewport, viewportData, viewportInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCornerstoneViewport(
|
public getCornerstoneViewport(
|
||||||
@ -261,12 +303,18 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
|
|
||||||
_setStackViewport(
|
_setStackViewport(
|
||||||
viewport: Types.IStackViewport,
|
viewport: Types.IStackViewport,
|
||||||
viewportData: StackData,
|
viewportData: StackViewportData,
|
||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo
|
||||||
) {
|
) {
|
||||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
const { imageIds, initialImageIndex } = viewportData;
|
const {
|
||||||
|
imageIds,
|
||||||
|
initialImageIndex,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
} = viewportData.data;
|
||||||
|
|
||||||
|
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
|
||||||
|
|
||||||
let initialImageIndexToUse = initialImageIndex;
|
let initialImageIndexToUse = initialImageIndex;
|
||||||
|
|
||||||
@ -328,7 +376,15 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (preset === JumpPresets.Middle) {
|
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;
|
return 0;
|
||||||
@ -336,7 +392,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
|
|
||||||
async _setVolumeViewport(
|
async _setVolumeViewport(
|
||||||
viewport: Types.IVolumeViewport,
|
viewport: Types.IVolumeViewport,
|
||||||
viewportData: VolumeData,
|
viewportData: VolumeViewportData,
|
||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: We need to overhaul the way data sources work so requests can be made
|
// TODO: We need to overhaul the way data sources work so requests can be made
|
||||||
@ -352,21 +408,24 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
// (This call may or may not create sub-requests for series metadata)
|
// (This call may or may not create sub-requests for series metadata)
|
||||||
const volumeInputArray = [];
|
const volumeInputArray = [];
|
||||||
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
const displaySetOptionsArray = viewportInfo.getDisplaySetOptions();
|
||||||
const { HangingProtocolService } = this;
|
const { HangingProtocolService } = this.servicesManager.services;
|
||||||
|
|
||||||
for (let i = 0; i < viewportData.imageIds.length; i++) {
|
const volumeToLoad = [];
|
||||||
const imageIds = viewportData.imageIds[i];
|
const displaySetInstanceUIDs = [];
|
||||||
const displaySetInstanceUID = viewportData.displaySetInstanceUIDs[i];
|
|
||||||
const displaySetOptions = displaySetOptionsArray[i];
|
|
||||||
|
|
||||||
const volumeId = displaySetInstanceUID;
|
for (const [index, data] of viewportData.data.entries()) {
|
||||||
|
const { volume, imageIds, displaySetInstanceUID } = data;
|
||||||
|
|
||||||
// if (displaySet.needsRerendering) {
|
displaySetInstanceUIDs.push(displaySetInstanceUID);
|
||||||
// console.warn('Removing volume from cache', volumeId);
|
|
||||||
// cache.removeVolumeLoadObject(volumeId);
|
if (!volume) {
|
||||||
// displaySet.needsRerendering = false;
|
continue;
|
||||||
// this.displaySetsNeedRerendering.add(displaySet.displaySetInstanceUID);
|
}
|
||||||
// }
|
|
||||||
|
volumeToLoad.push(volume);
|
||||||
|
|
||||||
|
const displaySetOptions = displaySetOptionsArray[index];
|
||||||
|
const { volumeId } = volume;
|
||||||
|
|
||||||
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
const voiCallbacks = this._getVOICallbacks(volumeId, displaySetOptions);
|
||||||
|
|
||||||
@ -383,6 +442,8 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.viewportsDisplaySets.set(viewport.id, displaySetInstanceUIDs);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
HangingProtocolService.hasCustomImageLoadStrategy() &&
|
HangingProtocolService.hasCustomImageLoadStrategy() &&
|
||||||
!HangingProtocolService.customImageLoadPerformed
|
!HangingProtocolService.customImageLoadPerformed
|
||||||
@ -394,7 +455,7 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportData.volumes.forEach(volume => {
|
volumeToLoad.forEach(volume => {
|
||||||
volume.load();
|
volume.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -403,9 +464,98 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async setVolumesForViewport(viewport, volumeInputArray) {
|
public async setVolumesForViewport(viewport, volumeInputArray) {
|
||||||
|
const {
|
||||||
|
DisplaySetService,
|
||||||
|
SegmentationService,
|
||||||
|
ToolGroupService,
|
||||||
|
} = this.servicesManager.services;
|
||||||
|
|
||||||
await viewport.setVolumes(volumeInputArray);
|
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 viewportInfo = this.getViewportInfo(viewport.id);
|
||||||
|
|
||||||
|
const toolGroup = ToolGroupService.getToolGroupForViewport(viewport.id);
|
||||||
|
csToolsUtils.segmentation.triggerSegmentationRender(toolGroup.id);
|
||||||
|
|
||||||
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
const initialImageOptions = viewportInfo.getInitialImageOptions();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -415,8 +565,9 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
) {
|
) {
|
||||||
const { index, preset } = initialImageOptions;
|
const { index, preset } = initialImageOptions;
|
||||||
|
|
||||||
const { numberOfSlices } =
|
const { numberOfSlices } = csUtils.getImageSliceDataForVolumeViewport(
|
||||||
csUtils.getImageSliceDataForVolumeViewport(viewport);
|
viewport
|
||||||
|
);
|
||||||
|
|
||||||
const imageIndex = this._getInitialImageIndex(
|
const imageIndex = this._getInitialImageIndex(
|
||||||
numberOfSlices,
|
numberOfSlices,
|
||||||
@ -488,18 +639,20 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setDisplaySets(
|
_setDisplaySets(
|
||||||
viewportId: string,
|
viewport: StackViewport | VolumeViewport,
|
||||||
viewportData: StackData | VolumeData,
|
viewportData: StackViewportData | VolumeViewportData,
|
||||||
viewportInfo: ViewportInfo
|
viewportInfo: ViewportInfo
|
||||||
): void {
|
): void {
|
||||||
const viewport = this.getCornerstoneViewport(viewportId);
|
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof StackViewport) {
|
||||||
this._setStackViewport(viewport, viewportData as StackData, viewportInfo);
|
this._setStackViewport(
|
||||||
|
viewport,
|
||||||
|
viewportData as StackViewportData,
|
||||||
|
viewportInfo
|
||||||
|
);
|
||||||
} else if (viewport instanceof VolumeViewport) {
|
} else if (viewport instanceof VolumeViewport) {
|
||||||
this._setVolumeViewport(
|
this._setVolumeViewport(
|
||||||
viewport,
|
viewport,
|
||||||
viewportData as VolumeData,
|
viewportData as VolumeViewportData,
|
||||||
viewportInfo
|
viewportInfo
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -574,6 +727,31 @@ class CornerstoneViewportService implements IViewportService {
|
|||||||
displaySetOptions: newDisplaySetOptions,
|
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) {
|
export default function ExtendedCornerstoneViewportService(serviceManager) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Types } from '@cornerstonejs/core';
|
import { Types } from '@cornerstonejs/core';
|
||||||
import { StackData, VolumeData } from './CornerstoneCacheService';
|
import { StackData, VolumeData } from '../../types/CornerstoneCacheService';
|
||||||
import {
|
import {
|
||||||
DisplaySetOptions,
|
DisplaySetOptions,
|
||||||
PublicViewportOptions,
|
PublicViewportOptions,
|
||||||
@ -31,7 +31,7 @@ export interface IViewportService {
|
|||||||
* @param {*} viewportIndex
|
* @param {*} viewportIndex
|
||||||
* @param {*} elementRef
|
* @param {*} elementRef
|
||||||
*/
|
*/
|
||||||
enableElement(
|
enableViewport(
|
||||||
viewportIndex: number,
|
viewportIndex: number,
|
||||||
viewportOptions: ViewportOptions,
|
viewportOptions: ViewportOptions,
|
||||||
elementRef: HTMLDivElement
|
elementRef: HTMLDivElement
|
||||||
@ -65,7 +65,7 @@ export interface IViewportService {
|
|||||||
* @param {*} dataSource
|
* @param {*} dataSource
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
setViewportDisplaySets(
|
setViewportData(
|
||||||
viewportIndex: number,
|
viewportIndex: number,
|
||||||
viewportData: StackData | VolumeData,
|
viewportData: StackData | VolumeData,
|
||||||
publicViewportOptions: PublicViewportOptions,
|
publicViewportOptions: PublicViewportOptions,
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { Types, Enums, CONSTANTS } from '@cornerstonejs/core';
|
import { Types, Enums } from '@cornerstonejs/core';
|
||||||
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
|
||||||
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
|
||||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||||
import JumpPresets from '../../utils/JumpPresets';
|
import JumpPresets from '../../utils/JumpPresets';
|
||||||
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
import { SyncGroup } from '../SyncGroupService/SyncGroupService';
|
||||||
|
import {
|
||||||
|
StackViewportData,
|
||||||
|
VolumeViewportData,
|
||||||
|
} from './CornerstoneCacheService';
|
||||||
|
|
||||||
export type InitialImageOptions = {
|
export type InitialImageOptions = {
|
||||||
index?: number;
|
index?: number;
|
||||||
@ -16,7 +20,6 @@ export type ViewportOptions = {
|
|||||||
viewportId: string;
|
viewportId: string;
|
||||||
orientation?: Types.Orientation;
|
orientation?: Types.Orientation;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
initialView?: string;
|
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: SyncGroup[];
|
||||||
initialImageOptions?: InitialImageOptions;
|
initialImageOptions?: InitialImageOptions;
|
||||||
customViewportProps?: Record<string, unknown>;
|
customViewportProps?: Record<string, unknown>;
|
||||||
@ -28,7 +31,6 @@ export type PublicViewportOptions = {
|
|||||||
viewportId?: string;
|
viewportId?: string;
|
||||||
orientation?: string;
|
orientation?: string;
|
||||||
background?: Types.Point3;
|
background?: Types.Point3;
|
||||||
initialView?: string;
|
|
||||||
syncGroups?: SyncGroup[];
|
syncGroups?: SyncGroup[];
|
||||||
initialImageOptions?: InitialImageOptions;
|
initialImageOptions?: InitialImageOptions;
|
||||||
customViewportProps?: Record<string, unknown>;
|
customViewportProps?: Record<string, unknown>;
|
||||||
@ -69,6 +71,7 @@ class ViewportInfo {
|
|||||||
private element: HTMLDivElement;
|
private element: HTMLDivElement;
|
||||||
private viewportOptions: ViewportOptions;
|
private viewportOptions: ViewportOptions;
|
||||||
private displaySetOptions: Array<DisplaySetOptions>;
|
private displaySetOptions: Array<DisplaySetOptions>;
|
||||||
|
private viewportData: StackViewportData | VolumeViewportData;
|
||||||
private renderingEngineId: string;
|
private renderingEngineId: string;
|
||||||
|
|
||||||
constructor(viewportIndex: number, viewportId: string) {
|
constructor(viewportIndex: number, viewportId: string) {
|
||||||
@ -78,6 +81,13 @@ class ViewportInfo {
|
|||||||
this.setPublicDisplaySetOptions([{}]);
|
this.setPublicDisplaySetOptions([{}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy = (): void => {
|
||||||
|
this.element = null;
|
||||||
|
this.viewportData = null;
|
||||||
|
this.viewportOptions = null;
|
||||||
|
this.displaySetOptions = null;
|
||||||
|
};
|
||||||
|
|
||||||
public setRenderingEngineId(renderingEngineId: string): void {
|
public setRenderingEngineId(renderingEngineId: string): void {
|
||||||
this.renderingEngineId = renderingEngineId;
|
this.renderingEngineId = renderingEngineId;
|
||||||
}
|
}
|
||||||
@ -97,6 +107,16 @@ class ViewportInfo {
|
|||||||
this.element = element;
|
this.element = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setViewportData(
|
||||||
|
viewportData: StackViewportData | VolumeViewportData
|
||||||
|
): void {
|
||||||
|
this.viewportData = viewportData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewportData(): StackViewportData | VolumeViewportData {
|
||||||
|
return this.viewportData;
|
||||||
|
}
|
||||||
|
|
||||||
public getViewportIndex(): number {
|
public getViewportIndex(): number {
|
||||||
return this.viewportIndex;
|
return this.viewportIndex;
|
||||||
}
|
}
|
||||||
@ -120,6 +140,23 @@ class ViewportInfo {
|
|||||||
this.setDisplaySetOptions(displaySetOptions);
|
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(
|
public setPublicViewportOptions(
|
||||||
viewportOptionsEntry: PublicViewportOptions
|
viewportOptionsEntry: PublicViewportOptions
|
||||||
): void {
|
): void {
|
||||||
@ -139,7 +176,7 @@ class ViewportInfo {
|
|||||||
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
|
if (viewportOptionsEntry.viewportType?.toLowerCase() === VOLUME) {
|
||||||
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
orientation = getCornerstoneOrientation(viewportOptionsEntry.orientation);
|
||||||
} else {
|
} else {
|
||||||
orientation = CONSTANTS.ORIENTATION.AXIAL;
|
orientation = Enums.OrientationAxis.AXIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toolGroupId) {
|
if (!toolGroupId) {
|
||||||
@ -203,12 +240,21 @@ class ViewportInfo {
|
|||||||
const displaySetOptions: Array<DisplaySetOptions> = [];
|
const displaySetOptions: Array<DisplaySetOptions> = [];
|
||||||
|
|
||||||
publicDisplaySetOptions.forEach(option => {
|
publicDisplaySetOptions.forEach(option => {
|
||||||
|
if (!option) {
|
||||||
|
option = {
|
||||||
|
blendMode: undefined,
|
||||||
|
slabThickness: undefined,
|
||||||
|
colormap: undefined,
|
||||||
|
voi: {},
|
||||||
|
voiInverted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
const blendMode = getCornerstoneBlendMode(option.blendMode);
|
const blendMode = getCornerstoneBlendMode(option.blendMode);
|
||||||
|
|
||||||
displaySetOptions.push({
|
displaySetOptions.push({
|
||||||
voi: option.voi || ({} as VOI),
|
voi: option.voi,
|
||||||
voiInverted: option.voiInverted || false,
|
voiInverted: option.voiInverted,
|
||||||
colormap: option.colormap || undefined,
|
colormap: option.colormap,
|
||||||
slabThickness: option.slabThickness,
|
slabThickness: option.slabThickness,
|
||||||
blendMode,
|
blendMode,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,4 +31,8 @@ const getEnabledElement = viewportIndex => {
|
|||||||
return state.enabledElements[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 React, { useEffect, useState } from 'react';
|
||||||
import domtoimage from 'dom-to-image';
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Enums,
|
Enums,
|
||||||
getEnabledElement,
|
getEnabledElement,
|
||||||
getOrCreateCanvas,
|
getOrCreateCanvas,
|
||||||
StackViewport,
|
StackViewport,
|
||||||
|
VolumeViewport,
|
||||||
} from '@cornerstonejs/core';
|
} from '@cornerstonejs/core';
|
||||||
import { ToolGroupManager } from '@cornerstonejs/tools';
|
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@ -17,7 +17,6 @@ const MINIMUM_SIZE = 100;
|
|||||||
const DEFAULT_SIZE = 512;
|
const DEFAULT_SIZE = 512;
|
||||||
const MAX_TEXTURE_SIZE = 10000;
|
const MAX_TEXTURE_SIZE = 10000;
|
||||||
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
|
const VIEWPORT_ID = 'cornerstone-viewport-download-form';
|
||||||
const TOOLGROUP_ID = 'cornerstone-viewport-download-form-toolgroup';
|
|
||||||
|
|
||||||
const CornerstoneViewportDownloadForm = ({
|
const CornerstoneViewportDownloadForm = ({
|
||||||
onClose,
|
onClose,
|
||||||
@ -26,15 +25,57 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
|
const enabledElement = OHIFgetEnabledElement(activeViewportIndex);
|
||||||
const activeViewportElement = enabledElement?.element;
|
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 => {
|
const enableViewport = viewportElement => {
|
||||||
if (viewportElement) {
|
if (viewportElement) {
|
||||||
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
const { renderingEngine, viewport } = getEnabledElement(
|
||||||
|
activeViewportElement
|
||||||
|
);
|
||||||
|
|
||||||
const viewportInput = {
|
const viewportInput = {
|
||||||
viewportId: VIEWPORT_ID,
|
viewportId: VIEWPORT_ID,
|
||||||
element: viewportElement,
|
element: viewportElement,
|
||||||
type: Enums.ViewportType.STACK,
|
type: viewport.type,
|
||||||
|
defaultOptions: {
|
||||||
|
background: viewport.defaultOptions.background,
|
||||||
|
orientation: viewport.defaultOptions.orientation,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
renderingEngine.enableElement(viewportInput);
|
renderingEngine.enableElement(viewportInput);
|
||||||
@ -43,11 +84,9 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
|
|
||||||
const disableViewport = viewportElement => {
|
const disableViewport = viewportElement => {
|
||||||
if (viewportElement) {
|
if (viewportElement) {
|
||||||
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
const { renderingEngine } = getEnabledElement(viewportElement);
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
renderingEngine.disableElement(VIEWPORT_ID);
|
renderingEngine.disableElement(VIEWPORT_ID);
|
||||||
ToolGroupManager.destroyToolGroup(TOOLGROUP_ID);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -115,26 +154,39 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
|
|
||||||
const { viewport } = activeViewportEnabledElement;
|
const { viewport } = activeViewportEnabledElement;
|
||||||
|
|
||||||
if (!(viewport instanceof StackViewport)) {
|
|
||||||
throw new Error('Viewport is not a StackViewport');
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageId = viewport.getCurrentImageId();
|
|
||||||
|
|
||||||
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
const renderingEngine = CornerstoneViewportService.getRenderingEngine();
|
||||||
const downloadViewport = renderingEngine.getViewport(
|
const downloadViewport = renderingEngine.getViewport(VIEWPORT_ID);
|
||||||
VIEWPORT_ID
|
|
||||||
) as StackViewport;
|
|
||||||
|
|
||||||
downloadViewport.setStack([imageId]).then(() => {
|
if (downloadViewport instanceof StackViewport) {
|
||||||
|
const imageId = viewport.getCurrentImageId();
|
||||||
const properties = viewport.getProperties();
|
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 newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||||
|
|
||||||
resolve({ width: newWidth, height: newHeight });
|
resolve({ width: newWidth, height: newHeight });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,31 +216,20 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
renderingEngineId
|
renderingEngineId
|
||||||
);
|
);
|
||||||
|
|
||||||
let downloadToolGroup = ToolGroupManager.getToolGroupForViewport(
|
// add the viewport to the toolGroup
|
||||||
downloadViewportId,
|
toolGroup.addViewport(downloadViewportId);
|
||||||
renderingEngineId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (downloadToolGroup === undefined) {
|
Object.keys(toolGroup._toolInstances).forEach(toolName => {
|
||||||
downloadToolGroup = ToolGroupManager.createToolGroup(TOOLGROUP_ID);
|
// make all tools Enabled so that they can not be interacted with
|
||||||
|
// in the download viewport
|
||||||
// what tools were in the active viewport?
|
if (toggle && toolName !== 'Crosshairs') {
|
||||||
// make them all enabled instances so that they can not be interacted
|
try {
|
||||||
// with in the download viewport
|
toolGroup.setToolEnabled(toolName);
|
||||||
Object.values(toolGroup._toolInstances).forEach(tool => {
|
} catch (e) {
|
||||||
downloadToolGroup.addTool(tool.getToolName());
|
console.log(e);
|
||||||
});
|
}
|
||||||
|
|
||||||
// add the viewport to the toolGroup
|
|
||||||
downloadToolGroup.addViewport(downloadViewportId);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.values(downloadToolGroup._toolInstances).forEach(tool => {
|
|
||||||
const toolName = tool.getToolName();
|
|
||||||
if (toggle) {
|
|
||||||
downloadToolGroup.setToolEnabled(toolName);
|
|
||||||
} else {
|
} else {
|
||||||
downloadToolGroup.setToolDisabled(toolName);
|
toolGroup.setToolDisabled(toolName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -199,10 +240,10 @@ const CornerstoneViewportDownloadForm = ({
|
|||||||
`div[data-viewport-uid="${VIEWPORT_ID}"]`
|
`div[data-viewport-uid="${VIEWPORT_ID}"]`
|
||||||
);
|
);
|
||||||
|
|
||||||
domtoimage.toPng(divForDownloadViewport).then(dataUrl => {
|
html2canvas(divForDownloadViewport).then(canvas => {
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.download = file;
|
link.download = file;
|
||||||
link.href = dataUrl;
|
link.href = canvas.toDataURL(fileType, 1.0);
|
||||||
link.click();
|
link.click();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,6 +38,10 @@ const getImageInstance = dataset => {
|
|||||||
return dataset && dataset.images && dataset.images[0];
|
return dataset && dataset.images && dataset.images[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getNonImageInstance = dataset => {
|
||||||
|
return dataset && dataset.instance;
|
||||||
|
};
|
||||||
|
|
||||||
const getImageInstanceId = imageInstance => {
|
const getImageInstanceId = imageInstance => {
|
||||||
return getImageId(imageInstance);
|
return getImageId(imageInstance);
|
||||||
};
|
};
|
||||||
@ -89,19 +93,28 @@ const getImageLoaderType = imageId => {
|
|||||||
|
|
||||||
class DicomLoaderService {
|
class DicomLoaderService {
|
||||||
getLocalData(dataset, studies) {
|
getLocalData(dataset, studies) {
|
||||||
if (dataset && dataset.localFile) {
|
// Use referenced imageInstance
|
||||||
// Use referenced imageInstance
|
const imageInstance = getImageInstance(dataset);
|
||||||
const imageInstance = getImageInstance(dataset);
|
const nonImageInstance = getNonImageInstance(dataset);
|
||||||
let imageId = getImageInstanceId(imageInstance);
|
|
||||||
|
|
||||||
// or Try to get it from studies
|
if (
|
||||||
if (someInvalidStrings(imageId)) {
|
(!imageInstance && !nonImageInstance) ||
|
||||||
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
|
!nonImageInstance.imageId.startsWith('dicomfile')
|
||||||
}
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!someInvalidStrings(imageId)) {
|
const instance = imageInstance || nonImageInstance;
|
||||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
|
||||||
}
|
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.getLocalData(dataset, studies);
|
||||||
yield this.getDataByImageType(dataset);
|
yield this.getDataByImageType(dataset);
|
||||||
yield this.getDataByDatasetType(dataset);
|
yield this.getDataByDatasetType(dataset);
|
||||||
}
|
}
|
||||||
|
|
||||||
findDicomDataPromise(dataset, studies) {
|
findDicomDataPromise(dataset, studies, headers) {
|
||||||
|
dataset.authorizationHeaders = headers;
|
||||||
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
||||||
// it returns first valid retriever method.
|
// it returns first valid retriever method.
|
||||||
for (const loader of loaderIterator) {
|
for (const loader of loaderIterator) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { CONSTANTS } from '@cornerstonejs/core';
|
import { Enums } from '@cornerstonejs/core';
|
||||||
import { log } from '@ohif/core';
|
import { log } from '@ohif/core';
|
||||||
|
|
||||||
const AXIAL = 'axial';
|
const AXIAL = 'axial';
|
||||||
@ -7,16 +7,20 @@ const CORONAL = 'coronal';
|
|||||||
|
|
||||||
export default function getCornerstoneOrientation(
|
export default function getCornerstoneOrientation(
|
||||||
orientation: string
|
orientation: string
|
||||||
): CONSTANTS.ORIENTATION {
|
): Enums.OrientationAxis {
|
||||||
switch (orientation.toLowerCase()) {
|
if (orientation) {
|
||||||
case AXIAL:
|
switch (orientation.toLowerCase()) {
|
||||||
return CONSTANTS.ORIENTATION.AXIAL;
|
case AXIAL:
|
||||||
case SAGITTAL:
|
return Enums.OrientationAxis.AXIAL;
|
||||||
return CONSTANTS.ORIENTATION.SAGITTAL;
|
case SAGITTAL:
|
||||||
case CORONAL:
|
return Enums.OrientationAxis.SAGITTAL;
|
||||||
return CONSTANTS.ORIENTATION.CORONAL;
|
case CORONAL:
|
||||||
default:
|
return Enums.OrientationAxis.CORONAL;
|
||||||
log.wanr('Choosing default orientation: axial');
|
default:
|
||||||
return CONSTANTS.ORIENTATION.AXIAL;
|
log.wanr('Choosing acquisition plane orientation');
|
||||||
|
return Enums.OrientationAxis.ACQUISITION;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Enums.OrientationAxis.ACQUISITION;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,8 +8,8 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This function caches the volumeUIDs until all the volumes inside the
|
* This function caches the volumeUIDs until all the volumes inside the
|
||||||
* hangging protocol are initialized. Then it goes through the imageIds
|
* hanging protocol are initialized. Then it goes through the imageIds
|
||||||
* of the volumes, and interleav them, in order for the volumes to be loaded
|
* of the volumes, and interleave them, in order for the volumes to be loaded
|
||||||
* together from middle to the start and the end.
|
* together from middle to the start and the end.
|
||||||
* @param {Object} props image loading properties from Cornerstone ViewportService
|
* @param {Object} props image loading properties from Cornerstone ViewportService
|
||||||
* @returns
|
* @returns
|
||||||
@ -17,7 +17,7 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
export default function interleaveCenterLoader({
|
export default function interleaveCenterLoader({
|
||||||
data: { viewportId, volumeInputArray },
|
data: { viewportId, volumeInputArray },
|
||||||
displaySetsMatchDetails,
|
displaySetsMatchDetails,
|
||||||
matchDetails,
|
viewportMatchDetails: matchDetails,
|
||||||
}) {
|
}) {
|
||||||
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
@ -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
|
* listen to it and as the other viewports are created we can set the volumes for them
|
||||||
* since volumes are already started loading.
|
* since volumes are already started loading.
|
||||||
*/
|
*/
|
||||||
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const viewportIdVolumeInputArrayMap = new Map<string, unknown[]>();
|
|||||||
export default function interleaveTopToBottom({
|
export default function interleaveTopToBottom({
|
||||||
data: { viewportId, volumeInputArray },
|
data: { viewportId, volumeInputArray },
|
||||||
displaySetsMatchDetails,
|
displaySetsMatchDetails,
|
||||||
matchDetails,
|
viewportMatchDetails: matchDetails,
|
||||||
}) {
|
}) {
|
||||||
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
viewportIdVolumeInputArrayMap.set(viewportId, volumeInputArray);
|
||||||
|
|
||||||
@ -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
|
* listen to it and as the other viewports are created we can set the volumes for them
|
||||||
* since volumes are already started loading.
|
* since volumes are already started loading.
|
||||||
*/
|
*/
|
||||||
if (matchDetails.length !== viewportIdVolumeInputArrayMap.size) {
|
if (matchDetails.size !== viewportIdVolumeInputArrayMap.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,6 +69,7 @@ const Length = {
|
|||||||
metadata,
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||||
toolName: metadata.toolName,
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: data.text,
|
label: data.text,
|
||||||
@ -90,13 +91,16 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
|||||||
|
|
||||||
const annotations = [];
|
const annotations = [];
|
||||||
|
|
||||||
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
const {
|
||||||
referencedImageId
|
SOPInstanceUID,
|
||||||
);
|
SeriesInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = getSOPInstanceAttributes(referencedImageId);
|
||||||
|
|
||||||
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
SeriesInstanceUID
|
SeriesInstanceUID,
|
||||||
|
frameNumber
|
||||||
);
|
);
|
||||||
|
|
||||||
const { SeriesNumber } = displaySet;
|
const { SeriesNumber } = displaySet;
|
||||||
@ -105,6 +109,7 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
|||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
SOPInstanceUID,
|
SOPInstanceUID,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
frameNumber,
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -119,7 +124,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
const displayText = [];
|
const displayText = [];
|
||||||
|
|
||||||
// Area is the same for all series
|
// Area is the same for all series
|
||||||
const { SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
const { SeriesNumber, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||||
|
|
||||||
const instance = displaySet.images.find(
|
const instance = displaySet.images.find(
|
||||||
image => image.SOPInstanceUID === SOPInstanceUID
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
@ -130,11 +135,10 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
InstanceNumber = instance.InstanceNumber;
|
InstanceNumber = instance.InstanceNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
displayText.push(
|
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||||
InstanceNumber
|
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||||
? `(S: ${SeriesNumber} I: ${InstanceNumber})`
|
|
||||||
: `(S: ${SeriesNumber})`
|
displayText.push(`(S: ${SeriesNumber}${instanceText}${frameText})`);
|
||||||
);
|
|
||||||
|
|
||||||
return displayText;
|
return displayText;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@ const Bidirectional = {
|
|||||||
metadata,
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||||
toolName: metadata.toolName,
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
@ -92,30 +93,33 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
|||||||
Object.keys(cachedStats).forEach(targetId => {
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
const targetStats = cachedStats[targetId];
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
let displaySet;
|
if (!referencedImageId) {
|
||||||
|
|
||||||
if (referencedImageId) {
|
|
||||||
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
|
||||||
referencedImageId
|
|
||||||
);
|
|
||||||
|
|
||||||
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
|
||||||
SOPInstanceUID,
|
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Non-acquisition plane measurement mapping not supported'
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = getSOPInstanceAttributes(referencedImageId);
|
||||||
|
|
||||||
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SeriesNumber } = displaySet;
|
||||||
const { length, width } = targetStats;
|
const { length, width } = targetStats;
|
||||||
const unit = 'mm';
|
const unit = 'mm';
|
||||||
|
|
||||||
annotations.push({
|
annotations.push({
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
frameNumber,
|
||||||
unit,
|
unit,
|
||||||
length,
|
length,
|
||||||
width,
|
width,
|
||||||
@ -171,7 +175,13 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
const displayText = [];
|
const displayText = [];
|
||||||
|
|
||||||
// Area is the same for all series
|
// Area is the same for all series
|
||||||
const { length, width, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
const {
|
||||||
|
length,
|
||||||
|
width,
|
||||||
|
SeriesNumber,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = mappedAnnotations[0];
|
||||||
const roundedLength = utils.roundNumber(length, 2);
|
const roundedLength = utils.roundNumber(length, 2);
|
||||||
const roundedWidth = utils.roundNumber(width, 2);
|
const roundedWidth = utils.roundNumber(width, 2);
|
||||||
|
|
||||||
@ -184,10 +194,11 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
InstanceNumber = instance.InstanceNumber;
|
InstanceNumber = instance.InstanceNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||||
|
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||||
|
|
||||||
displayText.push(
|
displayText.push(
|
||||||
InstanceNumber
|
`L: ${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||||
? `L: ${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
|
|
||||||
: `L: ${roundedLength} mm (S: ${SeriesNumber})`
|
|
||||||
);
|
);
|
||||||
displayText.push(`W: ${roundedWidth} mm`);
|
displayText.push(`W: ${roundedWidth} mm`);
|
||||||
|
|
||||||
|
|||||||
@ -66,6 +66,7 @@ const EllipticalROI = {
|
|||||||
metadata,
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||||
toolName: metadata.toolName,
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
@ -91,31 +92,34 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
|||||||
Object.keys(cachedStats).forEach(targetId => {
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
const targetStats = cachedStats[targetId];
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
let displaySet;
|
if (!referencedImageId) {
|
||||||
|
|
||||||
if (referencedImageId) {
|
|
||||||
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
|
||||||
referencedImageId
|
|
||||||
);
|
|
||||||
|
|
||||||
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
|
||||||
SOPInstanceUID,
|
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Todo: Non-acquisition plane measurement mapping not supported yet
|
// Todo: Non-acquisition plane measurement mapping not supported yet
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Non-acquisition plane measurement mapping not supported'
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = getSOPInstanceAttributes(referencedImageId);
|
||||||
|
|
||||||
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SeriesNumber } = displaySet;
|
||||||
const { mean, stdDev, max, area, Modality } = targetStats;
|
const { mean, stdDev, max, area, Modality } = targetStats;
|
||||||
const unit = getModalityUnit(Modality);
|
const unit = getModalityUnit(Modality);
|
||||||
|
|
||||||
annotations.push({
|
annotations.push({
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
frameNumber,
|
||||||
Modality,
|
Modality,
|
||||||
unit,
|
unit,
|
||||||
mean,
|
mean,
|
||||||
@ -184,7 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
const displayText = [];
|
const displayText = [];
|
||||||
|
|
||||||
// Area is the same for all series
|
// Area is the same for all series
|
||||||
const { area, SOPInstanceUID } = mappedAnnotations[0];
|
const { area, SOPInstanceUID, frameNumber } = mappedAnnotations[0];
|
||||||
|
|
||||||
const instance = displaySet.images.find(
|
const instance = displaySet.images.find(
|
||||||
image => image.SOPInstanceUID === SOPInstanceUID
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
@ -195,21 +199,26 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
InstanceNumber = instance.InstanceNumber;
|
InstanceNumber = instance.InstanceNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roundedArea = utils.roundNumber(area, 2);
|
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||||
|
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||||
|
|
||||||
|
// Area sometimes becomes undefined if `preventHandleOutsideImage` is off.
|
||||||
|
const roundedArea = utils.roundNumber(area || 0, 2);
|
||||||
displayText.push(`${roundedArea} mm<sup>2</sup>`);
|
displayText.push(`${roundedArea} mm<sup>2</sup>`);
|
||||||
|
|
||||||
// Todo: we need a better UI for displaying all these information
|
// Todo: we need a better UI for displaying all these information
|
||||||
mappedAnnotations.forEach(mappedAnnotation => {
|
mappedAnnotations.forEach(mappedAnnotation => {
|
||||||
const { unit, max, SeriesNumber } = mappedAnnotation;
|
const { unit, max, SeriesNumber } = mappedAnnotation;
|
||||||
|
|
||||||
|
let maxStr = '';
|
||||||
if (max) {
|
if (max) {
|
||||||
const roundedMax = utils.roundNumber(max, 2);
|
const roundedMax = utils.roundNumber(max, 2);
|
||||||
|
maxStr = `Max: ${roundedMax} <small>${unit}</small> `;
|
||||||
|
}
|
||||||
|
|
||||||
displayText.push(
|
const str = `${maxStr}(S:${SeriesNumber}${instanceText}${frameText})`;
|
||||||
InstanceNumber
|
if (!displayText.includes(str)) {
|
||||||
? `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber} I:${InstanceNumber})`
|
displayText.push(str);
|
||||||
: `Max: ${roundedMax} <small>${unit}</small> (S:${SeriesNumber})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,7 @@ const Length = {
|
|||||||
metadata,
|
metadata,
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
referenceStudyUID: StudyInstanceUID,
|
referenceStudyUID: StudyInstanceUID,
|
||||||
|
frameNumber: mappedAnnotations[0]?.frameNumber || 1,
|
||||||
toolName: metadata.toolName,
|
toolName: metadata.toolName,
|
||||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
@ -97,30 +98,33 @@ function getMappedAnnotations(annotation, DisplaySetService) {
|
|||||||
Object.keys(cachedStats).forEach(targetId => {
|
Object.keys(cachedStats).forEach(targetId => {
|
||||||
const targetStats = cachedStats[targetId];
|
const targetStats = cachedStats[targetId];
|
||||||
|
|
||||||
let displaySet;
|
if (!referencedImageId) {
|
||||||
|
|
||||||
if (referencedImageId) {
|
|
||||||
const { SOPInstanceUID, SeriesInstanceUID } = getSOPInstanceAttributes(
|
|
||||||
referencedImageId
|
|
||||||
);
|
|
||||||
|
|
||||||
displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
|
||||||
SOPInstanceUID,
|
|
||||||
SeriesInstanceUID
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Non-acquisition plane measurement mapping not supported'
|
'Non-acquisition plane measurement mapping not supported'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SeriesNumber, SeriesInstanceUID } = displaySet;
|
const {
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = getSOPInstanceAttributes(referencedImageId);
|
||||||
|
|
||||||
|
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
frameNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const { SeriesNumber } = displaySet;
|
||||||
const { length } = targetStats;
|
const { length } = targetStats;
|
||||||
const unit = 'mm';
|
const unit = 'mm';
|
||||||
|
|
||||||
annotations.push({
|
annotations.push({
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
|
frameNumber,
|
||||||
unit,
|
unit,
|
||||||
length,
|
length,
|
||||||
});
|
});
|
||||||
@ -175,7 +179,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
const displayText = [];
|
const displayText = [];
|
||||||
|
|
||||||
// Area is the same for all series
|
// Area is the same for all series
|
||||||
const { length, SeriesNumber, SOPInstanceUID } = mappedAnnotations[0];
|
const {
|
||||||
|
length,
|
||||||
|
SeriesNumber,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameNumber,
|
||||||
|
} = mappedAnnotations[0];
|
||||||
|
|
||||||
const instance = displaySet.images.find(
|
const instance = displaySet.images.find(
|
||||||
image => image.SOPInstanceUID === SOPInstanceUID
|
image => image.SOPInstanceUID === SOPInstanceUID
|
||||||
@ -186,11 +195,12 @@ function getDisplayText(mappedAnnotations, displaySet) {
|
|||||||
InstanceNumber = instance.InstanceNumber;
|
InstanceNumber = instance.InstanceNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
|
||||||
|
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
|
||||||
|
|
||||||
const roundedLength = utils.roundNumber(length, 2);
|
const roundedLength = utils.roundNumber(length, 2);
|
||||||
displayText.push(
|
displayText.push(
|
||||||
InstanceNumber
|
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
|
||||||
? `${roundedLength} mm (S: ${SeriesNumber} I: ${InstanceNumber})`
|
|
||||||
: `${roundedLength} mm (S: ${SeriesNumber})`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return displayText;
|
return displayText;
|
||||||
|
|||||||
@ -45,6 +45,7 @@ function _getUIDFromImageID(imageId) {
|
|||||||
SOPInstanceUID: instance.SOPInstanceUID,
|
SOPInstanceUID: instance.SOPInstanceUID,
|
||||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
StudyInstanceUID: instance.StudyInstanceUID,
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
|
frameNumber: instance.frameNumber || 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
285
extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts
Normal file
285
extensions/cornerstone/src/utils/mpr/toggleMPRHangingProtocol.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import { Enums } from '@cornerstonejs/tools';
|
||||||
|
import removeToolGroupSegmentationRepresentations from '../removeToolGroupSegmentationRepresentations';
|
||||||
|
|
||||||
|
const MPR_TOOLGROUP_ID = 'mpr';
|
||||||
|
|
||||||
|
const cachedState = {
|
||||||
|
protocol: null,
|
||||||
|
stage: null,
|
||||||
|
viewportMatchDetails: null,
|
||||||
|
viewportStructure: null,
|
||||||
|
toolOptions: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCachedState = (
|
||||||
|
protocol,
|
||||||
|
stage,
|
||||||
|
viewportMatchDetails,
|
||||||
|
viewportStructure,
|
||||||
|
toolOptions
|
||||||
|
) => {
|
||||||
|
cachedState.protocol = protocol;
|
||||||
|
cachedState.stage = stage;
|
||||||
|
cachedState.viewportMatchDetails = viewportMatchDetails;
|
||||||
|
cachedState.viewportStructure = viewportStructure;
|
||||||
|
cachedState.toolOptions = JSON.parse(JSON.stringify(toolOptions));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetCachedState = () => {
|
||||||
|
cachedState.protocol = null;
|
||||||
|
cachedState.stage = null;
|
||||||
|
cachedState.viewportMatchDetails = null;
|
||||||
|
cachedState.viewportStructure = null;
|
||||||
|
cachedState.toolOptions = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function toggleMPRHangingProtocol({
|
||||||
|
toggledState,
|
||||||
|
servicesManager,
|
||||||
|
getToolGroup,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
UINotificationService,
|
||||||
|
HangingProtocolService,
|
||||||
|
ViewportGridService,
|
||||||
|
ToolBarService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
const {
|
||||||
|
activeViewportIndex,
|
||||||
|
viewports,
|
||||||
|
numRows,
|
||||||
|
numCols,
|
||||||
|
} = 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// What is the current active protocol and stage number to restore later
|
||||||
|
const { protocol, stage } = HangingProtocolService.getActiveProtocol();
|
||||||
|
|
||||||
|
if (toggledState) {
|
||||||
|
resetCachedState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
viewportMatchDetails,
|
||||||
|
viewportStructure,
|
||||||
|
toolOptions,
|
||||||
|
} = _getViewportsInfo({
|
||||||
|
protocol,
|
||||||
|
stage,
|
||||||
|
viewports,
|
||||||
|
servicesManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
setCachedState(
|
||||||
|
protocol,
|
||||||
|
stage,
|
||||||
|
viewportMatchDetails,
|
||||||
|
viewportStructure,
|
||||||
|
toolOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchDetails = {
|
||||||
|
displaySetInstanceUIDs: viewportDisplaySetInstanceUIDs,
|
||||||
|
};
|
||||||
|
|
||||||
|
_disableCrosshairs(
|
||||||
|
toolOptions.map(({ toolGroupId }) => toolGroupId),
|
||||||
|
getToolGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
HangingProtocolService.setProtocol(
|
||||||
|
MPR_TOOLGROUP_ID,
|
||||||
|
matchDetails,
|
||||||
|
errorCallback
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreErrorCallback = error => {
|
||||||
|
UINotificationService.show({
|
||||||
|
title: 'Multiplanar reconstruction (MPR) ',
|
||||||
|
message:
|
||||||
|
'Something went wrong while trying to restore the previous layout.',
|
||||||
|
type: 'info',
|
||||||
|
duration: 3000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_disableCrosshairs([MPR_TOOLGROUP_ID], getToolGroup);
|
||||||
|
|
||||||
|
const { layoutType, properties } = cachedState.viewportStructure;
|
||||||
|
const { viewportMatchDetails } = cachedState;
|
||||||
|
|
||||||
|
// The reason we split the flow here is that we don't allow viewport grid
|
||||||
|
// change in the non default hanging protocol, so we can just apply the
|
||||||
|
// cached protocol and stage. However, for the default protocol, we need
|
||||||
|
// to also apply the layout type and properties.
|
||||||
|
if (cachedState.protocol.id !== 'default') {
|
||||||
|
HangingProtocolService.setProtocol(
|
||||||
|
cachedState.protocol.id,
|
||||||
|
viewportMatchDetails,
|
||||||
|
restoreErrorCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HangingProtocolService.setProtocol(
|
||||||
|
'default',
|
||||||
|
viewportMatchDetails,
|
||||||
|
restoreErrorCallback
|
||||||
|
);
|
||||||
|
|
||||||
|
if (numRows !== properties.rows || numCols !== properties.columns) {
|
||||||
|
ViewportGridService.setLayout({
|
||||||
|
numRows: properties.rows,
|
||||||
|
numCols: properties.columns,
|
||||||
|
layoutType,
|
||||||
|
layoutOptions: properties.layoutOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const numViewports =
|
||||||
|
properties.layoutOptions.length || properties.rows * properties.columns;
|
||||||
|
|
||||||
|
// loop inside viewportMatchDetails map
|
||||||
|
// and set the viewportOptions for each viewport
|
||||||
|
[...Array(numViewports).keys()].forEach(viewportIndex => {
|
||||||
|
const viewportMatchDetailsForViewport = viewportMatchDetails.get(
|
||||||
|
viewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
if (viewportMatchDetailsForViewport) {
|
||||||
|
const {
|
||||||
|
viewportOptions,
|
||||||
|
displaySetsInfo,
|
||||||
|
} = viewportMatchDetailsForViewport;
|
||||||
|
ViewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportIndex,
|
||||||
|
displaySetInstanceUIDs: displaySetsInfo.map(
|
||||||
|
displaySetInfo => displaySetInfo.displaySetInstanceUID
|
||||||
|
),
|
||||||
|
viewportOptions,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ViewportGridService.setDisplaySetsForViewport({
|
||||||
|
viewportIndex,
|
||||||
|
displaySetInstanceUIDs: [],
|
||||||
|
viewportOptions: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _disableCrosshairs(toolGroupIds, getToolGroup) {
|
||||||
|
toolGroupIds.forEach(toolGroupId => {
|
||||||
|
const toolGroup = getToolGroup(toolGroupId);
|
||||||
|
if (
|
||||||
|
toolGroup.getToolInstance('Crosshairs')?.mode === Enums.ToolModes.Active
|
||||||
|
) {
|
||||||
|
toolGroup.setToolDisabled('Crosshairs');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getViewportsInfo({ protocol, stage, viewports, servicesManager }) {
|
||||||
|
// here we need to use the viewports and try to map it into the
|
||||||
|
// viewportMatchDetails and displaySetMatch that HangingProtocolService
|
||||||
|
// expects
|
||||||
|
const {
|
||||||
|
ViewportGridService,
|
||||||
|
HangingProtocolService,
|
||||||
|
ToolGroupService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
const { numRows, numCols } = ViewportGridService.getState();
|
||||||
|
|
||||||
|
let viewportMatchDetails = new Map();
|
||||||
|
|
||||||
|
const viewportStructure = {
|
||||||
|
layoutType: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: numRows,
|
||||||
|
columns: numCols,
|
||||||
|
layoutOptions: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
viewports.forEach((viewport, viewportIndex) => {
|
||||||
|
viewportStructure.properties.layoutOptions.push({
|
||||||
|
x: viewport.x,
|
||||||
|
y: viewport.y,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (protocol.id === 'default') {
|
||||||
|
viewports.forEach((viewport, viewportIndex) => {
|
||||||
|
if (viewport.displaySetInstanceUIDs) {
|
||||||
|
viewportMatchDetails.set(viewportIndex, {
|
||||||
|
displaySetsInfo: viewport.displaySetInstanceUIDs.map(
|
||||||
|
displaySetInstanceUID => {
|
||||||
|
return { displaySetInstanceUID };
|
||||||
|
}
|
||||||
|
),
|
||||||
|
viewportOptions: viewport.viewportOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
({ viewportMatchDetails } = HangingProtocolService.getMatchDetails());
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the toolGroup state for viewports
|
||||||
|
let toolOptions = [];
|
||||||
|
const viewportIds = viewports
|
||||||
|
.map(
|
||||||
|
viewport =>
|
||||||
|
viewport.displaySetInstanceUIDs &&
|
||||||
|
viewport.displaySetInstanceUIDs.length > 0 &&
|
||||||
|
viewport.viewportOptions?.viewportId
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (viewportIds.length) {
|
||||||
|
toolOptions = viewportIds.map(viewportId => {
|
||||||
|
const toolGroup = ToolGroupService.getToolGroupForViewport(viewportId);
|
||||||
|
return {
|
||||||
|
toolGroupId: toolGroup.id,
|
||||||
|
toolOptions: toolGroup.toolOptions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { viewportMatchDetails, viewportStructure, toolOptions };
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
@ -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;
|
||||||
|
};
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import calculateViewportRegistrations from './calculateViewportRegistrations';
|
||||||
|
|
||||||
|
// [ {
|
||||||
|
// synchronizerId: string,
|
||||||
|
// viewports: [ { viewportId: number, renderingEngineId: string, index: number } , ...]
|
||||||
|
// ]}
|
||||||
|
let STACK_IMAGE_SYNC_GROUPS_INFO = [];
|
||||||
|
|
||||||
|
export default function toggleStackImageSync({
|
||||||
|
toggledState,
|
||||||
|
servicesManager,
|
||||||
|
getEnabledElement,
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
SyncGroupService,
|
||||||
|
ViewportGridService,
|
||||||
|
DisplaySetService,
|
||||||
|
CornerstoneViewportService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,7 +32,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/i18n": "^1.0.0",
|
"@ohif/i18n": "^1.0.0",
|
||||||
"dcmjs": "^0.24.5",
|
"dcmjs": "^0.28.3",
|
||||||
"dicomweb-client": "^0.6.0",
|
"dicomweb-client": "^0.6.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@ -43,6 +43,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.16.3",
|
"@babel/runtime": "7.16.3",
|
||||||
"@cornerstonejs/calculate-suv": "1.0.2"
|
"@cornerstonejs/calculate-suv": "^1.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
import getImageId from '../DicomWebDataSource/utils/getImageId';
|
import getImageId from '../DicomWebDataSource/utils/getImageId';
|
||||||
|
import getDirectURL from '../utils/getDirectURL';
|
||||||
|
|
||||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ const findStudies = (key, value) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createDicomJSONApi(dicomJsonConfig) {
|
function createDicomJSONApi(dicomJsonConfig) {
|
||||||
const { name } = dicomJsonConfig;
|
const { name, wadoRoot } = dicomJsonConfig;
|
||||||
|
|
||||||
const implementation = {
|
const implementation = {
|
||||||
initialize: async ({ params, query, url }) => {
|
initialize: async ({ params, query, url }) => {
|
||||||
@ -93,7 +94,7 @@ function createDicomJSONApi(dicomJsonConfig) {
|
|||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: () => {},
|
mapParams: () => { },
|
||||||
search: async param => {
|
search: async param => {
|
||||||
const [key, value] = Object.entries(param)[0];
|
const [key, value] = Object.entries(param)[0];
|
||||||
const mappedParam = mappings[key];
|
const mappedParam = mappings[key];
|
||||||
@ -133,6 +134,22 @@ function createDicomJSONApi(dicomJsonConfig) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
|
/**
|
||||||
|
* Generates a URL that can be used for direct retrieve of the bulkdata
|
||||||
|
*
|
||||||
|
* @param {object} params
|
||||||
|
* @param {string} params.tag is the tag name of the URL to retrieve
|
||||||
|
* @param {string} params.defaultPath path for the pixel data url
|
||||||
|
* @param {object} params.instance is the instance object that the tag is in
|
||||||
|
* @param {string} params.defaultType is the mime type of the response
|
||||||
|
* @param {string} params.singlepart is the type of the part to retrieve
|
||||||
|
* @param {string} params.fetchPart unknown?
|
||||||
|
* @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart,
|
||||||
|
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
||||||
|
*/
|
||||||
|
directURL: params => {
|
||||||
|
return getDirectURL(wadoRoot, params);
|
||||||
|
},
|
||||||
series: {
|
series: {
|
||||||
metadata: ({
|
metadata: ({
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
const implementation = {
|
const implementation = {
|
||||||
initialize: ({ params, query }) => {
|
initialize: ({ params, query }) => {
|
||||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||||
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
|
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
|
||||||
|
|
||||||
const StudyInstanceUIDs =
|
const StudyInstanceUIDs =
|
||||||
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
||||||
@ -166,7 +166,6 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
|
|
||||||
displaySet.images.forEach(instance => {
|
displaySet.images.forEach(instance => {
|
||||||
const NumberOfFrames = instance.NumberOfFrames;
|
const NumberOfFrames = instance.NumberOfFrames;
|
||||||
|
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
for (let i = 0; i < NumberOfFrames; i++) {
|
for (let i = 0; i < NumberOfFrames; i++) {
|
||||||
const imageId = this.getImageIdsForInstance({
|
const imageId = this.getImageIdsForInstance({
|
||||||
@ -190,9 +189,14 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
SOPInstanceUID
|
SOPInstanceUID
|
||||||
);
|
);
|
||||||
if (storedInstance.url) {
|
|
||||||
return storedInstance.url;
|
let imageId = storedInstance.url;
|
||||||
|
|
||||||
|
if (frame !== undefined) {
|
||||||
|
imageId += `&frame=${frame}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return imageId;
|
||||||
},
|
},
|
||||||
deleteStudyMetadataPromise() {
|
deleteStudyMetadataPromise() {
|
||||||
console.log('deleteStudyMetadataPromise not implemented');
|
console.log('deleteStudyMetadataPromise not implemented');
|
||||||
|
|||||||
53
extensions/default/src/DicomTagBrowser/DicomTagBrowser.css
Normal file
53
extensions/default/src/DicomTagBrowser/DicomTagBrowser.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
.dicom-tag-browser-table {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-table-wrapper {
|
||||||
|
/* height: 500px;*/
|
||||||
|
/*overflow-y: scroll;*/
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-table tr {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
color: #ffffff;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stick {
|
||||||
|
position: sticky;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-content {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
/*height: 500px;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-instance-range .range {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-instance-range {
|
||||||
|
padding: 20px 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-table td.dicom-tag-browser-table-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-table th {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: "#20A5D6";
|
||||||
|
}
|
||||||
|
|
||||||
|
.dicom-tag-browser-table th.dicom-tag-browser-table-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
327
extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
Normal file
327
extensions/default/src/DicomTagBrowser/DicomTagBrowser.tsx
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import dcmjs from 'dcmjs';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { classes } from '@ohif/core';
|
||||||
|
import DicomTagTable from './DicomTagTable';
|
||||||
|
import './DicomTagBrowser.css';
|
||||||
|
import { InputRange, Select, Typography } from '@ohif/ui';
|
||||||
|
|
||||||
|
const { ImageSet } = classes;
|
||||||
|
const { DicomMetaDictionary } = dcmjs.data;
|
||||||
|
const { nameMap } = DicomMetaDictionary;
|
||||||
|
|
||||||
|
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||||
|
const [
|
||||||
|
selectedDisplaySetInstanceUID,
|
||||||
|
setSelectedDisplaySetInstanceUID,
|
||||||
|
] = useState(displaySetInstanceUID);
|
||||||
|
const [instanceNumber, setInstanceNumber] = useState(1);
|
||||||
|
|
||||||
|
const onSelectChange = value => {
|
||||||
|
setSelectedDisplaySetInstanceUID(value.value);
|
||||||
|
setInstanceNumber(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeDisplaySet = displaySets.find(
|
||||||
|
ds => ds.displaySetInstanceUID === selectedDisplaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const isImageStack = _isImageStack(activeDisplaySet);
|
||||||
|
const showInstanceList = isImageStack && activeDisplaySet.images.length > 1;
|
||||||
|
|
||||||
|
const displaySetList = useMemo(() => {
|
||||||
|
displaySets.sort((a, b) => a.SeriesNumber - b.SeriesNumber);
|
||||||
|
return displaySets.map(displaySet => {
|
||||||
|
const {
|
||||||
|
displaySetInstanceUID,
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesNumber,
|
||||||
|
SeriesDescription,
|
||||||
|
Modality,
|
||||||
|
} = displaySet;
|
||||||
|
|
||||||
|
/* Map to display representation */
|
||||||
|
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||||
|
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||||
|
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: displaySetInstanceUID,
|
||||||
|
label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
|
||||||
|
description: displayDate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [displaySets]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dicom-tag-browser-content">
|
||||||
|
<div className="flex flex-row items-center mb-2">
|
||||||
|
<Typography variant="subtitle" className="w-1/2 mr-8">
|
||||||
|
Series
|
||||||
|
</Typography>
|
||||||
|
{showInstanceList && (
|
||||||
|
<Typography variant="subtitle" className="w-1/2">
|
||||||
|
Instance Number
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center mb-6">
|
||||||
|
<div className="w-1/2 mr-8">
|
||||||
|
<Select
|
||||||
|
isClearable={false}
|
||||||
|
onChange={onSelectChange}
|
||||||
|
options={displaySetList}
|
||||||
|
value={displaySetList.find(
|
||||||
|
ds => ds.value === selectedDisplaySetInstanceUID
|
||||||
|
)}
|
||||||
|
className="text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{showInstanceList ? (
|
||||||
|
<div className="w-1/2">
|
||||||
|
<InputRange
|
||||||
|
value={instanceNumber}
|
||||||
|
onChange={value => {
|
||||||
|
setInstanceNumber(parseInt(value));
|
||||||
|
}}
|
||||||
|
minValue={1}
|
||||||
|
maxValue={activeDisplaySet.images.length}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<DicomTagTable
|
||||||
|
rows={getFormattedRowsFromTags(activeDisplaySet, instanceNumber)}
|
||||||
|
></DicomTagTable>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getFormattedRowsFromTags(displaySet, instanceNumber) {
|
||||||
|
const isImageStack = _isImageStack(displaySet);
|
||||||
|
|
||||||
|
let metadata;
|
||||||
|
|
||||||
|
if (isImageStack) {
|
||||||
|
metadata = displaySet.images[instanceNumber - 1];
|
||||||
|
} else {
|
||||||
|
metadata = displaySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = getSortedTags(metadata);
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
tags.forEach(tagInfo => {
|
||||||
|
if (tagInfo.vr === 'SQ') {
|
||||||
|
rows.push([
|
||||||
|
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
||||||
|
tagInfo.vr,
|
||||||
|
tagInfo.keyword,
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { values } = tagInfo;
|
||||||
|
|
||||||
|
values.forEach((item, index) => {
|
||||||
|
const formatedRowsFromTags = getFormattedRowsFromTags(item);
|
||||||
|
|
||||||
|
rows.push([
|
||||||
|
`${item[0].tagIndent}(FFFE,E000)`,
|
||||||
|
'',
|
||||||
|
`Item #${index}`,
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
|
||||||
|
rows.push(...formatedRowsFromTags);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (tagInfo.vr === 'xs') {
|
||||||
|
try {
|
||||||
|
/* const dataset = metadataProvider.getStudyDataset(
|
||||||
|
meta.StudyInstanceUID
|
||||||
|
);*/
|
||||||
|
// console.log(dataset);
|
||||||
|
// const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
|
||||||
|
// const originalTagInfo = dataset[tag];
|
||||||
|
// tagInfo.vr = originalTagInfo.vr;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to parse value representation for tag '${tagInfo.keyword}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagInfo.vr === 'PN') {
|
||||||
|
rows.push([
|
||||||
|
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
||||||
|
tagInfo.vr,
|
||||||
|
tagInfo.keyword,
|
||||||
|
tagInfo.value,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
rows.push([
|
||||||
|
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
||||||
|
tagInfo.vr,
|
||||||
|
tagInfo.keyword,
|
||||||
|
tagInfo.value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortedTags(metadata) {
|
||||||
|
const tagList = getRows(metadata);
|
||||||
|
|
||||||
|
// Sort top level tags, sequence groups are sorted when created.
|
||||||
|
_sortTagList(tagList);
|
||||||
|
|
||||||
|
return tagList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRows(metadata, depth = 0) {
|
||||||
|
// Tag, Type, Value, Keyword
|
||||||
|
|
||||||
|
const keywords = Object.keys(metadata);
|
||||||
|
|
||||||
|
let tagIndent = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < depth; i++) {
|
||||||
|
tagIndent += '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth > 0) {
|
||||||
|
tagIndent += ' '; // If indented, add a space after the indents.
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 0; i < keywords.length; i++) {
|
||||||
|
let keyword = keywords[i];
|
||||||
|
|
||||||
|
if (keyword === '_vrMap') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagInfo = nameMap[keyword];
|
||||||
|
|
||||||
|
let value = metadata[keyword];
|
||||||
|
|
||||||
|
if (tagInfo && tagInfo.vr === 'SQ') {
|
||||||
|
const sequenceAsArray = toArray(value);
|
||||||
|
|
||||||
|
// Push line defining the sequence
|
||||||
|
|
||||||
|
const sequence = {
|
||||||
|
tag: tagInfo.tag,
|
||||||
|
tagIndent,
|
||||||
|
vr: tagInfo.vr,
|
||||||
|
keyword,
|
||||||
|
values: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rows.push(sequence);
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
// Type 2 Sequence
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sequenceAsArray.forEach(item => {
|
||||||
|
const sequenceRows = getRows(item, depth + 1);
|
||||||
|
|
||||||
|
if (sequenceRows.length) {
|
||||||
|
// Sort the sequence group.
|
||||||
|
_sortTagList(sequenceRows);
|
||||||
|
sequence.values.push(sequenceRows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length > 0 && typeof value[0] != 'object') {
|
||||||
|
value = value.join('\\');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
value = value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
if (value === null) {
|
||||||
|
value = ' ';
|
||||||
|
} else {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
if (value.InlineBinary) {
|
||||||
|
value = 'Inline Binary';
|
||||||
|
} else if (value.BulkDataURI) {
|
||||||
|
value = `Bulk Data URI`; //: ${value.BulkDataURI}`;
|
||||||
|
} else if (value.Alphabetic) {
|
||||||
|
value = value.Alphabetic;
|
||||||
|
} else {
|
||||||
|
console.warn(`Unrecognised Value: ${value} for ${keyword}:`);
|
||||||
|
console.warn(value);
|
||||||
|
value = ' ';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Unrecognised Value: ${value} for ${keyword}:`);
|
||||||
|
value = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tag / vr/ keyword/ value
|
||||||
|
|
||||||
|
// Remove retired tags
|
||||||
|
keyword = keyword.replace('RETIRED_', '');
|
||||||
|
if (tagInfo) {
|
||||||
|
rows.push({
|
||||||
|
tag: tagInfo.tag,
|
||||||
|
tagIndent,
|
||||||
|
vr: tagInfo.vr,
|
||||||
|
keyword,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// skip properties without hex tag numbers
|
||||||
|
const regex = /[0-9A-Fa-f]{6}/g;
|
||||||
|
if (keyword.match(regex)) {
|
||||||
|
const tag = `(${keyword.substring(0, 4)},${keyword.substring(4, 8)})`;
|
||||||
|
rows.push({
|
||||||
|
tag,
|
||||||
|
tagIndent,
|
||||||
|
vr: '',
|
||||||
|
keyword: 'Private Tag',
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isImageStack(displaySet) {
|
||||||
|
return displaySet instanceof ImageSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArray(objectOrArray) {
|
||||||
|
return Array.isArray(objectOrArray) ? objectOrArray : [objectOrArray];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _sortTagList(tagList) {
|
||||||
|
tagList.sort((a, b) => {
|
||||||
|
if (a.tag < b.tag) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DicomTagBrowser;
|
||||||
100
extensions/default/src/DicomTagBrowser/DicomTagTable.tsx
Normal file
100
extensions/default/src/DicomTagBrowser/DicomTagTable.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ColumnHeaders() {
|
||||||
|
return (
|
||||||
|
<div className="m-auto flex flex-col py-2 bg-secondary-light">
|
||||||
|
<div className="flex flex-row w-full">
|
||||||
|
<div className="px-3 w-5/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Tag
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 w-4/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
VR
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 w-6/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Keyword
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 w-5/24">
|
||||||
|
<label className="flex flex-col flex-1 text-white text-lg pl-1 select-none">
|
||||||
|
<span className="flex flex-row items-center focus:outline-none">
|
||||||
|
Value
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DicomTagTable({ rows }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ColumnHeaders()}
|
||||||
|
<div
|
||||||
|
className="m-auto relative border-2 border-secondary-light overflow-hidden ohif-scrollbar"
|
||||||
|
style={{ height: '32rem' }}
|
||||||
|
>
|
||||||
|
<table className="w-full text-white">
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, index) => {
|
||||||
|
const className = row.className ? row.className : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className="hover:bg-secondary-main transition duration-300 bg-primary-dark"
|
||||||
|
key={`DICOMTagRow-${index}`}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
style={{ maxWidth: '0px' }}
|
||||||
|
className="px-4 py-2 text-base break-all border-b border-secondary-light w-5/24"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="inline-flex max-w-full">{row[0]}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{ maxWidth: '0px' }}
|
||||||
|
className="px-4 py-2 text-base break-all border-b border-secondary-light w-4/24"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="inline-flex max-w-full">{row[1]}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{ maxWidth: '0px' }}
|
||||||
|
className="px-4 py-2 text-base break-all border-b border-secondary-light w-6/24"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="inline-flex max-w-full">{row[2]}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{ maxWidth: '0px' }}
|
||||||
|
className="px-4 py-2 text-base break-all border-b border-secondary-light w-14/24"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="inline-flex max-w-full">{row[3]}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DicomTagTable;
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
} from './retrieveStudyMetadata.js';
|
} from './retrieveStudyMetadata.js';
|
||||||
import StaticWadoClient from './utils/StaticWadoClient.js';
|
import StaticWadoClient from './utils/StaticWadoClient.js';
|
||||||
|
import getDirectURL from '../utils/getDirectURL.js';
|
||||||
|
|
||||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
|
|
||||||
const wadoConfig = {
|
const wadoConfig = {
|
||||||
url: wadoRoot,
|
url: wadoRoot,
|
||||||
|
staticWado,
|
||||||
singlepart,
|
singlepart,
|
||||||
headers: UserAuthenticationService.getAuthorizationHeader(),
|
headers: UserAuthenticationService.getAuthorizationHeader(),
|
||||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||||
@ -80,15 +82,19 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
const qidoDicomWebClient = staticWado
|
const qidoDicomWebClient = staticWado
|
||||||
? new StaticWadoClient(qidoConfig)
|
? new StaticWadoClient(qidoConfig)
|
||||||
: new api.DICOMwebClient(qidoConfig);
|
: new api.DICOMwebClient(qidoConfig);
|
||||||
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
|
|
||||||
|
const wadoDicomWebClient = staticWado
|
||||||
|
? new StaticWadoClient(wadoConfig)
|
||||||
|
: new api.DICOMwebClient(wadoConfig);
|
||||||
|
|
||||||
const implementation = {
|
const implementation = {
|
||||||
initialize: ({ params, query }) => {
|
initialize: ({ params, query }) => {
|
||||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||||
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
|
const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs');
|
||||||
|
|
||||||
const StudyInstanceUIDs =
|
const StudyInstanceUIDs =
|
||||||
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
(queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) ||
|
||||||
|
paramsStudyInstanceUIDs;
|
||||||
const StudyInstanceUIDsAsArray =
|
const StudyInstanceUIDsAsArray =
|
||||||
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
||||||
? StudyInstanceUIDs
|
? StudyInstanceUIDs
|
||||||
@ -98,7 +104,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
query: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: mapParams.bind(),
|
mapParams: mapParams.bind(),
|
||||||
search: async function(origParams) {
|
search: async function (origParams) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
qidoDicomWebClient.headers = headers;
|
qidoDicomWebClient.headers = headers;
|
||||||
@ -123,7 +129,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
// mapParams: mapParams.bind(),
|
||||||
search: async function(studyInstanceUid) {
|
search: async function (studyInstanceUid) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
qidoDicomWebClient.headers = headers;
|
qidoDicomWebClient.headers = headers;
|
||||||
@ -168,65 +174,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
||||||
*/
|
*/
|
||||||
directURL: params => {
|
directURL: params => {
|
||||||
const {
|
return getDirectURL(wadoRoot, params);
|
||||||
instance,
|
|
||||||
tag = 'PixelData',
|
|
||||||
defaultPath = '/pixeldata',
|
|
||||||
defaultType = 'video/mp4',
|
|
||||||
singlepart: fetchPart = 'video',
|
|
||||||
} = params;
|
|
||||||
const value = instance[tag];
|
|
||||||
if (!value) return undefined;
|
|
||||||
|
|
||||||
if (value.DirectRetrieveURL) return value.DirectRetrieveURL;
|
|
||||||
if (value.InlineBinary) {
|
|
||||||
const blob = utils.b64toBlob(value.InlineBinary, defaultType);
|
|
||||||
value.DirectRetrieveURL = URL.createObjectURL(blob);
|
|
||||||
return value.DirectRetrieveURL;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!singlepart ||
|
|
||||||
(singlepart !== true && singlepart.indexOf(fetchPart) === -1)
|
|
||||||
) {
|
|
||||||
if (value.retrieveBulkData) {
|
|
||||||
return value.retrieveBulkData().then(arr => {
|
|
||||||
value.DirectRetrieveURL = URL.createObjectURL(
|
|
||||||
new Blob([arr], { type: defaultType })
|
|
||||||
);
|
|
||||||
return value.DirectRetrieveURL;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.warn('Unable to retrieve', tag, 'from', instance);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
} = instance;
|
|
||||||
const BulkDataURI =
|
|
||||||
(value && value.BulkDataURI) ||
|
|
||||||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
|
|
||||||
const hasQuery = BulkDataURI.indexOf('?') != -1;
|
|
||||||
const hasAccept = BulkDataURI.indexOf('accept=') != -1;
|
|
||||||
const acceptUri =
|
|
||||||
BulkDataURI +
|
|
||||||
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
|
|
||||||
if (BulkDataURI.indexOf('http') === 0) return acceptUri;
|
|
||||||
if (BulkDataURI.indexOf('/') === 0) {
|
|
||||||
return wadoRoot + acceptUri;
|
|
||||||
}
|
|
||||||
if (BulkDataURI.indexOf('series/') == 0) {
|
|
||||||
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
|
|
||||||
}
|
|
||||||
if (BulkDataURI.indexOf('instances/') === 0) {
|
|
||||||
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
|
|
||||||
}
|
|
||||||
if (BulkDataURI.indexOf('bulkdata/') === 0) {
|
|
||||||
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
|
|
||||||
}
|
|
||||||
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
|
|
||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
metadata: async ({
|
metadata: async ({
|
||||||
@ -418,6 +366,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
// any implementation that stores static copies of the metadata
|
// any implementation that stores static copies of the metadata
|
||||||
StudyInstanceUID: naturalized.StudyInstanceUID,
|
StudyInstanceUID: naturalized.StudyInstanceUID,
|
||||||
};
|
};
|
||||||
|
// Todo: this needs to be from wado dicom web client
|
||||||
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
||||||
const ret = (val && val[0]) || undefined;
|
const ret = (val && val[0]) || undefined;
|
||||||
value.Value = ret;
|
value.Value = ret;
|
||||||
@ -435,6 +384,9 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
|
|
||||||
// Adding instanceMetadata to OHIF MetadataProvider
|
// Adding instanceMetadata to OHIF MetadataProvider
|
||||||
naturalizedInstances.forEach((instance, index) => {
|
naturalizedInstances.forEach((instance, index) => {
|
||||||
|
instance.wadoRoot = dicomWebConfig.wadoRoot;
|
||||||
|
instance.wadoUri = dicomWebConfig.wadoUri;
|
||||||
|
|
||||||
const imageId = implementation.getImageIdsForInstance({
|
const imageId = implementation.getImageIdsForInstance({
|
||||||
instance,
|
instance,
|
||||||
});
|
});
|
||||||
@ -473,12 +425,13 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
|
|
||||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||||
|
|
||||||
const numberOfSeries = seriesPromises.length;
|
const seriesDeliveredPromises = seriesPromises.map(promise =>
|
||||||
seriesPromises.forEach(async (seriesPromise, index) => {
|
promise.then(instances => {
|
||||||
const instances = await seriesPromise;
|
storeInstances(instances);
|
||||||
storeInstances(instances);
|
})
|
||||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
);
|
||||||
});
|
await Promise.all(seriesDeliveredPromises);
|
||||||
|
setSuccessFlag();
|
||||||
},
|
},
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet(displaySet) {
|
getImageIdsForDisplaySet(displaySet) {
|
||||||
@ -493,10 +446,10 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
const NumberOfFrames = instance.NumberOfFrames;
|
const NumberOfFrames = instance.NumberOfFrames;
|
||||||
|
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
for (let i = 0; i < NumberOfFrames; i++) {
|
for (let frame = 1; frame <= NumberOfFrames; frame++) {
|
||||||
const imageId = this.getImageIdsForInstance({
|
const imageId = this.getImageIdsForInstance({
|
||||||
instance,
|
instance,
|
||||||
frame: i,
|
frame,
|
||||||
});
|
});
|
||||||
imageIds.push(imageId);
|
imageIds.push(imageId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -171,7 +171,7 @@ function mapParams(params, options = {}) {
|
|||||||
// Named
|
// Named
|
||||||
PatientName: withWildcard(params.patientName),
|
PatientName: withWildcard(params.patientName),
|
||||||
//PatientID: withWildcard(params.patientId),
|
//PatientID: withWildcard(params.patientId),
|
||||||
"00100020": withWildcard(params.patientId), // Temporarily to make the tests pass with dicomweb-server.. Apparently it's broken?
|
'00100020': withWildcard(params.patientId), // Temporarily to make the tests pass with dicomweb-server.. Apparently it's broken?
|
||||||
AccessionNumber: withWildcard(params.accessionNumber),
|
AccessionNumber: withWildcard(params.accessionNumber),
|
||||||
StudyDescription: withWildcard(params.studyDescription),
|
StudyDescription: withWildcard(params.studyDescription),
|
||||||
ModalitiesInStudy: params.modalitiesInStudy,
|
ModalitiesInStudy: params.modalitiesInStudy,
|
||||||
|
|||||||
@ -8,14 +8,19 @@ import { api } from 'dicomweb-client';
|
|||||||
* by manually implementing a query option.
|
* by manually implementing a query option.
|
||||||
*/
|
*/
|
||||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||||
static filterKeys = {
|
static studyFilterKeys = {
|
||||||
"StudyInstanceUID": "0020000D",
|
StudyInstanceUID: '0020000D',
|
||||||
"PatientName": "00100010",
|
PatientName: '00100010',
|
||||||
"00100020": "mrn",
|
'00100020': 'mrn',
|
||||||
"StudyDescription": "00081030",
|
StudyDescription: '00081030',
|
||||||
"StudyDate": "00080020",
|
StudyDate: '00080020',
|
||||||
"ModalitiesInStudy": "00080061",
|
ModalitiesInStudy: '00080061',
|
||||||
AccessionNumber: "00080050",
|
AccessionNumber: '00080050',
|
||||||
|
};
|
||||||
|
|
||||||
|
static seriesFilterKeys = {
|
||||||
|
SeriesInstanceUID: '0020000E',
|
||||||
|
SeriesNumber: '00200011',
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(qidoConfig) {
|
constructor(qidoConfig) {
|
||||||
@ -36,14 +41,48 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
const { queryParams } = options;
|
const { queryParams } = options;
|
||||||
if (!queryParams) return searchResult;
|
if (!queryParams) return searchResult;
|
||||||
const filtered = searchResult.filter(study => {
|
const filtered = searchResult.filter(study => {
|
||||||
for (const key of Object.keys(StaticWadoClient.filterKeys)) {
|
for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) {
|
||||||
if (!this.filterItem(key, queryParams, study)) return false;
|
if (
|
||||||
|
!this.filterItem(
|
||||||
|
key,
|
||||||
|
queryParams,
|
||||||
|
study,
|
||||||
|
StaticWadoClient.studyFilterKeys
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchForSeries(options) {
|
||||||
|
if (!this.staticWado) return super.searchForSeries(options);
|
||||||
|
|
||||||
|
let searchResult = await super.searchForSeries(options);
|
||||||
|
const { queryParams } = options;
|
||||||
|
if (!queryParams) return searchResult;
|
||||||
|
const filtered = searchResult.filter(study => {
|
||||||
|
for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) {
|
||||||
|
if (
|
||||||
|
!this.filterItem(
|
||||||
|
key,
|
||||||
|
queryParams,
|
||||||
|
study,
|
||||||
|
StaticWadoClient.seriesFilterKeys
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares values, matching any instance of desired to any instance of
|
* Compares values, matching any instance of desired to any instance of
|
||||||
* actual by recursively go through the paired set of values. That is,
|
* actual by recursively go through the paired set of values. That is,
|
||||||
@ -66,7 +105,7 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
if (actual?.Alphabetic) {
|
if (actual?.Alphabetic) {
|
||||||
actual = actual.Alphabetic;
|
actual = actual.Alphabetic;
|
||||||
}
|
}
|
||||||
if (typeof (actual) == 'string') {
|
if (typeof actual == 'string') {
|
||||||
if (actual.length === 0) return true;
|
if (actual.length === 0) return true;
|
||||||
if (desired.length === 0 || desired === '*') return true;
|
if (desired.length === 0 || desired === '*') return true;
|
||||||
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
|
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
|
||||||
@ -75,7 +114,10 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
} else if (desired[desired.length - 1] === '*') {
|
} else if (desired[desired.length - 1] === '*') {
|
||||||
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
|
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
|
||||||
} else if (desired[0] === '*') {
|
} else if (desired[0] === '*') {
|
||||||
return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1;
|
return (
|
||||||
|
actual.indexOf(desired.substring(1)) ===
|
||||||
|
actual.length - desired.length + 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return desired === actual;
|
return desired === actual;
|
||||||
@ -88,8 +130,7 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
if (dash === -1) return this.compareValues(range, value);
|
if (dash === -1) return this.compareValues(range, value);
|
||||||
const start = range.substring(0, dash);
|
const start = range.substring(0, dash);
|
||||||
const end = range.substring(dash + 1);
|
const end = range.substring(dash + 1);
|
||||||
return (!start || value >= start) &&
|
return (!start || value >= start) && (!end || value <= end);
|
||||||
(!end || value <= end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,16 +139,19 @@ export default class StaticWadoClient extends api.DICOMwebClient {
|
|||||||
* @param {*} key
|
* @param {*} key
|
||||||
* @param {*} queryParams
|
* @param {*} queryParams
|
||||||
* @param {*} study
|
* @param {*} study
|
||||||
|
* @param {*} sourceFilterMap
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
filterItem(key, queryParams, study) {
|
filterItem(key, queryParams, study, sourceFilterMap) {
|
||||||
const altKey = StaticWadoClient.filterKeys[key] || key;
|
const altKey = sourceFilterMap[key] || key;
|
||||||
if (!queryParams) return true;
|
if (!queryParams) return true;
|
||||||
const testValue = queryParams[key] || queryParams[altKey];
|
const testValue = queryParams[key] || queryParams[altKey];
|
||||||
if (!testValue) return true;
|
if (!testValue) return true;
|
||||||
const valueElem = study[key] || study[altKey];
|
const valueElem = study[key] || study[altKey];
|
||||||
if (!valueElem) return false;
|
if (!valueElem) return false;
|
||||||
if (valueElem.vr == 'DA') return this.compareDateRange(testValue, valueElem.Value[0]);
|
if (valueElem.vr == 'DA') {
|
||||||
|
return this.compareDateRange(testValue, valueElem.Value[0]);
|
||||||
|
}
|
||||||
const value = valueElem.Value;
|
const value = valueElem.Value;
|
||||||
return this.compareValues(testValue, value) && true;
|
return this.compareValues(testValue, value) && true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,14 +8,6 @@ import { utils } from '@ohif/core';
|
|||||||
|
|
||||||
const { downloadCSVReport } = utils;
|
const { downloadCSVReport } = utils;
|
||||||
|
|
||||||
// tools with measurements to display inside the panel
|
|
||||||
const MEASUREMENT_TOOLS = [
|
|
||||||
'EllipticalROI',
|
|
||||||
'RectangleROI',
|
|
||||||
'Length',
|
|
||||||
'Bidirectional',
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function PanelMeasurementTable({
|
export default function PanelMeasurementTable({
|
||||||
servicesManager,
|
servicesManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
@ -155,7 +147,6 @@ export default function PanelMeasurementTable({
|
|||||||
>
|
>
|
||||||
<MeasurementTable
|
<MeasurementTable
|
||||||
title="Measurements"
|
title="Measurements"
|
||||||
amount={displayMeasurements.length}
|
|
||||||
data={displayMeasurements}
|
data={displayMeasurements}
|
||||||
onClick={jumpToImage}
|
onClick={jumpToImage}
|
||||||
onEdit={onMeasurementItemEditHandler}
|
onEdit={onMeasurementItemEditHandler}
|
||||||
@ -186,12 +177,8 @@ PanelMeasurementTable.propTypes = {
|
|||||||
|
|
||||||
function _getMappedMeasurements(MeasurementService) {
|
function _getMappedMeasurements(MeasurementService) {
|
||||||
const measurements = MeasurementService.getMeasurements();
|
const measurements = MeasurementService.getMeasurements();
|
||||||
// filter out measurements whose toolName is not in MEASUREMENT_TOOLS
|
|
||||||
const measurementTools = measurements.filter(measurement =>
|
|
||||||
MEASUREMENT_TOOLS.includes(measurement.toolName)
|
|
||||||
);
|
|
||||||
|
|
||||||
const mappedMeasurements = measurementTools.map((m, index) =>
|
const mappedMeasurements = measurements.map((m, index) =>
|
||||||
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
|
_mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -10,12 +10,17 @@ const { formatDate } = utils;
|
|||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
*/
|
*/
|
||||||
function PanelStudyBrowser({
|
function PanelStudyBrowser({
|
||||||
DisplaySetService,
|
servicesManager,
|
||||||
getImageSrc,
|
getImageSrc,
|
||||||
getStudiesForPatientByStudyInstanceUID,
|
getStudiesForPatientByStudyInstanceUID,
|
||||||
requestDisplaySetCreationForStudy,
|
requestDisplaySetCreationForStudy,
|
||||||
dataSource,
|
dataSource,
|
||||||
}) {
|
}) {
|
||||||
|
const {
|
||||||
|
HangingProtocolService,
|
||||||
|
DisplaySetService,
|
||||||
|
UINotificationService,
|
||||||
|
} = servicesManager.services;
|
||||||
// Normally you nest the components so the tree isn't so deep, and the data
|
// 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.
|
// doesn't have to have such an intense shape. This works well enough for now.
|
||||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||||
@ -34,10 +39,25 @@ function PanelStudyBrowser({
|
|||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
|
|
||||||
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
const onDoubleClickThumbnailHandler = displaySetInstanceUID => {
|
||||||
viewportGridService.setDisplaySetsForViewport({
|
let updatedViewports = [];
|
||||||
viewportIndex: activeViewportIndex,
|
const viewportIndex = activeViewportIndex;
|
||||||
displaySetInstanceUIDs: [displaySetInstanceUID],
|
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
|
// ~~ studyDisplayList
|
||||||
@ -61,7 +81,19 @@ function PanelStudyBrowser({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
setStudyDisplayList(actuallyMappedStudies);
|
setStudyDisplayList(prevArray => {
|
||||||
|
const ret = [...prevArray];
|
||||||
|
for (const study of actuallyMappedStudies) {
|
||||||
|
if (
|
||||||
|
!prevArray.find(
|
||||||
|
it => it.studyInstanceUid === study.studyInstanceUid
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ret.push(study);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,12 +247,7 @@ function PanelStudyBrowser({
|
|||||||
}
|
}
|
||||||
|
|
||||||
PanelStudyBrowser.propTypes = {
|
PanelStudyBrowser.propTypes = {
|
||||||
DisplaySetService: PropTypes.shape({
|
servicesManager: PropTypes.object.isRequired,
|
||||||
EVENTS: PropTypes.object.isRequired,
|
|
||||||
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
getDisplaySetByUID: PropTypes.func.isRequired,
|
|
||||||
subscribe: PropTypes.func.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
dataSource: PropTypes.shape({
|
dataSource: PropTypes.shape({
|
||||||
getImageIdsForDisplaySet: PropTypes.func.isRequired,
|
getImageIdsForDisplaySet: PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
|||||||
@ -35,7 +35,7 @@ function WrappedPanelStudyBrowser({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelStudyBrowser
|
<PanelStudyBrowser
|
||||||
DisplaySetService={servicesManager.services.DisplaySetService}
|
servicesManager={servicesManager}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
getImageSrc={_getImageSrcFromImageId}
|
getImageSrc={_getImageSrcFromImageId}
|
||||||
getStudiesForPatientByStudyInstanceUID={
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,6 +2,6 @@ import React from 'react';
|
|||||||
|
|
||||||
export default function ToolbarDivider() {
|
export default function ToolbarDivider() {
|
||||||
return (
|
return (
|
||||||
<span className="self-center w-1 h-8 mx-2 border-l border-common-dark" />
|
<span className="self-center w-4 h-8 mx-2 border-l border-common-dark" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,43 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import {
|
||||||
LayoutSelector as OHIFLayoutSelector,
|
LayoutSelector as OHIFLayoutSelector,
|
||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
function LayoutSelector() {
|
function LayoutSelector({ rows, columns, servicesManager }) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [disableSelector, setDisableSelector] = useState(false);
|
||||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||||
|
|
||||||
|
const { HangingProtocolService } = servicesManager.services;
|
||||||
|
|
||||||
const closeOnOutsideClick = () => {
|
const closeOnOutsideClick = () => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setIsOpen(false);
|
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(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('click', closeOnOutsideClick);
|
window.addEventListener('click', closeOnOutsideClick);
|
||||||
return () => {
|
return () => {
|
||||||
@ -41,16 +64,30 @@ function LayoutSelector() {
|
|||||||
dropdownContent={
|
dropdownContent={
|
||||||
DropdownContent !== null && (
|
DropdownContent !== null && (
|
||||||
<DropdownContent
|
<DropdownContent
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
onSelection={({ numRows, numCols }) => {
|
onSelection={({ numRows, numCols }) => {
|
||||||
viewportGridService.setLayout({ numCols, numRows });
|
viewportGridService.setLayout({ numRows, numCols });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isActive={isOpen}
|
isActive={disableSelector ? false : isOpen}
|
||||||
type="toggle"
|
type="toggle"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LayoutSelector.propTypes = {
|
||||||
|
rows: PropTypes.number,
|
||||||
|
columns: PropTypes.number,
|
||||||
|
onLayoutChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
LayoutSelector.defaultProps = {
|
||||||
|
rows: 3,
|
||||||
|
columns: 3,
|
||||||
|
onLayoutChange: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
export default LayoutSelector;
|
export default LayoutSelector;
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SidePanel,
|
SidePanel,
|
||||||
ErrorBoundary,
|
ErrorBoundary,
|
||||||
@ -9,74 +11,15 @@ import {
|
|||||||
AboutModal,
|
AboutModal,
|
||||||
Header,
|
Header,
|
||||||
useModal,
|
useModal,
|
||||||
|
LoadingIndicatorProgress,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import { hotkeys } from '@ohif/core';
|
import { hotkeys } from '@ohif/core';
|
||||||
import { useAppConfig } from '@state';
|
import { useAppConfig } from '@state';
|
||||||
|
import Toolbar from '../Toolbar/Toolbar';
|
||||||
|
|
||||||
const { availableLanguages, defaultLanguage, currentLanguage } = i18n;
|
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({
|
function ViewerLayout({
|
||||||
// From Extension Module Params
|
// From Extension Module Params
|
||||||
extensionManager,
|
extensionManager,
|
||||||
@ -84,21 +27,39 @@ function ViewerLayout({
|
|||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
// From Modes
|
// From Modes
|
||||||
leftPanels,
|
|
||||||
rightPanels,
|
|
||||||
viewports,
|
viewports,
|
||||||
ViewportGridComp,
|
ViewportGridComp,
|
||||||
|
leftPanels = [],
|
||||||
|
rightPanels = [],
|
||||||
|
leftPanelDefaultClosed = false,
|
||||||
|
rightPanelDefaultClosed = false,
|
||||||
}) {
|
}) {
|
||||||
const [appConfig] = useAppConfig();
|
const [appConfig] = useAppConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const onClickReturnButton = () => {
|
const onClickReturnButton = () => {
|
||||||
navigate('/');
|
const { pathname } = location;
|
||||||
|
const dataSourceIdx = pathname.indexOf('/', 1);
|
||||||
|
const search =
|
||||||
|
dataSourceIdx === -1
|
||||||
|
? undefined
|
||||||
|
: `datasourcename=${pathname.substring(dataSourceIdx + 1)}`;
|
||||||
|
navigate({
|
||||||
|
pathname: '/',
|
||||||
|
search,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { show, hide } = useModal();
|
const { show, hide } = useModal();
|
||||||
|
|
||||||
|
const [showLoadingIndicator, setShowLoadingIndicator] = useState(
|
||||||
|
appConfig.showLoadingIndicator
|
||||||
|
);
|
||||||
|
|
||||||
|
const { HangingProtocolService } = servicesManager.services;
|
||||||
|
|
||||||
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager;
|
||||||
const versionNumber = process.env.VERSION_NUMBER;
|
const versionNumber = process.env.VERSION_NUMBER;
|
||||||
const buildNumber = process.env.BUILD_NUM;
|
const buildNumber = process.env.BUILD_NUM;
|
||||||
@ -146,6 +107,18 @@ function ViewerLayout({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (appConfig.oidc) {
|
||||||
|
menuOptions.push({
|
||||||
|
title: t('Header:Logout'),
|
||||||
|
icon: 'power-off',
|
||||||
|
onClick: async () => {
|
||||||
|
navigate(
|
||||||
|
`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set body classes (tailwindcss) that don't allow vertical
|
* Set body classes (tailwindcss) that don't allow vertical
|
||||||
* or horizontal overflow (no scrolling). Also guarantee window
|
* or horizontal overflow (no scrolling). Also guarantee window
|
||||||
@ -160,10 +133,29 @@ function ViewerLayout({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getPanelData = id => {
|
const getComponent = id => {
|
||||||
const entry = extensionManager.getModuleEntry(id);
|
const entry = extensionManager.getModuleEntry(id);
|
||||||
// TODO, not sure why sidepanel content has to be JSX, and not a children prop?
|
|
||||||
const content = entry.component;
|
if (!entry) {
|
||||||
|
throw new Error(
|
||||||
|
`${id} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (entry && entry.component) {
|
||||||
|
content = entry.component;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`No component found from extension ${id}. Check the reference string to the extension in your Mode configuration`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entry, content };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPanelData = id => {
|
||||||
|
const { content, entry } = getComponent(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
iconName: entry.iconName,
|
iconName: entry.iconName,
|
||||||
@ -174,8 +166,27 @@ 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 getViewportComponentData = viewportComponent => {
|
||||||
const entry = extensionManager.getModuleEntry(viewportComponent.namespace);
|
const { entry } = getComponent(viewportComponent.namespace);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
component: entry.component,
|
component: entry.component,
|
||||||
@ -201,40 +212,45 @@ function ViewerLayout({
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Header>
|
</Header>
|
||||||
<div
|
<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' }}
|
style={{ height: 'calc(100vh - 52px' }}
|
||||||
>
|
>
|
||||||
{/* LEFT SIDEPANELS */}
|
<React.Fragment>
|
||||||
{leftPanelComponents.length ? (
|
{showLoadingIndicator && (
|
||||||
<ErrorBoundary context="Left Panel">
|
<LoadingIndicatorProgress className="h-full w-full bg-black" />
|
||||||
<SidePanel
|
)}
|
||||||
side="left"
|
{/* LEFT SIDEPANELS */}
|
||||||
defaultComponentOpen={leftPanelComponents[0].name}
|
{leftPanelComponents.length ? (
|
||||||
childComponents={leftPanelComponents}
|
<ErrorBoundary context="Left Panel">
|
||||||
/>
|
<SidePanel
|
||||||
</ErrorBoundary>
|
side="left"
|
||||||
) : null}
|
activeTabIndex={leftPanelDefaultClosed ? null : 0}
|
||||||
{/* TOOLBAR + GRID */}
|
tabs={leftPanelComponents}
|
||||||
<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}
|
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</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>
|
||||||
</div>
|
{rightPanelComponents.length ? (
|
||||||
{rightPanelComponents.length ? (
|
<ErrorBoundary context="Right Panel">
|
||||||
<ErrorBoundary context="Right Panel">
|
<SidePanel
|
||||||
<SidePanel
|
side="right"
|
||||||
side="right"
|
activeTabIndex={rightPanelDefaultClosed ? null : 0}
|
||||||
defaultComponentOpen={rightPanelComponents[0].name}
|
tabs={rightPanelComponents}
|
||||||
childComponents={rightPanelComponents}
|
/>
|
||||||
/>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
) : null}
|
||||||
) : null}
|
</React.Fragment>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -249,13 +265,10 @@ ViewerLayout.propTypes = {
|
|||||||
// From modes
|
// From modes
|
||||||
leftPanels: PropTypes.array,
|
leftPanels: PropTypes.array,
|
||||||
rightPanels: PropTypes.array,
|
rightPanels: PropTypes.array,
|
||||||
|
leftPanelDefaultClosed: PropTypes.bool.isRequired,
|
||||||
|
rightPanelDefaultClosed: PropTypes.bool.isRequired,
|
||||||
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
||||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewerLayout.defaultProps = {
|
|
||||||
leftPanels: [],
|
|
||||||
rightPanels: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewerLayout;
|
export default ViewerLayout;
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
|
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||||
const {
|
const {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
|
ViewportGridService,
|
||||||
|
DisplaySetService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -23,6 +28,25 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
previousStage: () => {
|
previousStage: () => {
|
||||||
HangingProtocolService.previousProtocolStage();
|
HangingProtocolService.previousProtocolStage();
|
||||||
},
|
},
|
||||||
|
openDICOMTagViewer() {
|
||||||
|
const { activeViewportIndex, viewports } = ViewportGridService.getState();
|
||||||
|
const activeViewportSpecificData = viewports[activeViewportIndex];
|
||||||
|
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
||||||
|
|
||||||
|
const displaySets = DisplaySetService.activeDisplaySets;
|
||||||
|
const { UIModalService } = servicesManager.services;
|
||||||
|
|
||||||
|
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
||||||
|
UIModalService.show({
|
||||||
|
content: DicomTagBrowser,
|
||||||
|
contentProps: {
|
||||||
|
displaySets,
|
||||||
|
displaySetInstanceUID,
|
||||||
|
onClose: UIModalService.hide,
|
||||||
|
},
|
||||||
|
title: 'DICOM Tag Browser',
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
@ -46,6 +70,9 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
openDICOMTagViewer: {
|
||||||
|
commandFn: actions.openDICOMTagViewer,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -9,25 +9,26 @@ const defaultProtocol = {
|
|||||||
editableBy: {},
|
editableBy: {},
|
||||||
protocolMatchingRules: [],
|
protocolMatchingRules: [],
|
||||||
toolGroupIds: ['default'],
|
toolGroupIds: ['default'],
|
||||||
|
displaySetSelectors: {
|
||||||
|
defaultDisplaySetId: {
|
||||||
|
// Unused currently
|
||||||
|
imageMatchingRules: [],
|
||||||
|
// Matches displaysets, NOT series
|
||||||
|
seriesMatchingRules: [],
|
||||||
|
studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
stages: [
|
stages: [
|
||||||
{
|
{
|
||||||
id: 'hYbmMy3b7pz7GLiaT',
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
name: 'default',
|
name: 'default',
|
||||||
viewportStructure: {
|
viewportStructure: {
|
||||||
type: 'grid',
|
layoutType: 'grid',
|
||||||
properties: {
|
properties: {
|
||||||
rows: 1,
|
rows: 1,
|
||||||
columns: 1,
|
columns: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
displaySets: [
|
|
||||||
{
|
|
||||||
id: 'displaySet',
|
|
||||||
imageMatchingRules: [],
|
|
||||||
seriesMatchingRules: [],
|
|
||||||
studyMatchingRules: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
viewports: [
|
viewports: [
|
||||||
{
|
{
|
||||||
viewportOptions: {
|
viewportOptions: {
|
||||||
@ -40,7 +41,7 @@ const defaultProtocol = {
|
|||||||
displaySets: [
|
displaySets: [
|
||||||
{
|
{
|
||||||
options: [],
|
options: [],
|
||||||
id: 'displaySet',
|
id: 'defaultDisplaySetId',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -54,8 +55,8 @@ const defaultProtocol = {
|
|||||||
function getHangingProtocolModule() {
|
function getHangingProtocolModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'default',
|
id: defaultProtocol.id,
|
||||||
protocols: [defaultProtocol],
|
protocol: defaultProtocol,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,9 +34,10 @@ function getPanelModule({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'measure',
|
name: 'measure',
|
||||||
iconName: 'list-bullets',
|
iconName: 'tab-linear',
|
||||||
iconLabel: 'Measure',
|
iconLabel: 'Measure',
|
||||||
label: 'Measurements',
|
label: 'Measurements',
|
||||||
|
secondaryLabel: 'Measurements',
|
||||||
component: wrappedMeasurementPanel,
|
component: wrappedMeasurementPanel,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { isImage } from '@ohif/core/src/utils/isImage';
|
import { isImage } from '@ohif/core/src/utils/isImage';
|
||||||
|
import sopClassDictionary from '@ohif/core/src/utils/sopClassDictionary';
|
||||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||||
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
@ -24,6 +25,7 @@ const makeDisplaySet = instances => {
|
|||||||
StudyInstanceUID: instance.StudyInstanceUID,
|
StudyInstanceUID: instance.StudyInstanceUID,
|
||||||
SeriesNumber: instance.SeriesNumber || 0,
|
SeriesNumber: instance.SeriesNumber || 0,
|
||||||
FrameRate: instance.FrameTime,
|
FrameRate: instance.FrameTime,
|
||||||
|
SOPClassUID: instance.SOPClassUID,
|
||||||
SeriesDescription: instance.SeriesDescription || '',
|
SeriesDescription: instance.SeriesDescription || '',
|
||||||
Modality: instance.Modality,
|
Modality: instance.Modality,
|
||||||
isMultiFrame: isMultiFrame(instance),
|
isMultiFrame: isMultiFrame(instance),
|
||||||
@ -118,7 +120,7 @@ function getDisplaySetsFromSeries(instances) {
|
|||||||
acquisitionDatetime: instance.AcquisitionDateTime,
|
acquisitionDatetime: instance.AcquisitionDateTime,
|
||||||
});
|
});
|
||||||
displaySets.push(displaySet);
|
displaySets.push(displaySet);
|
||||||
} else if (isSingleImageModality(instance.modality)) {
|
} else if (isSingleImageModality(instance.Modality)) {
|
||||||
displaySet = makeDisplaySet([instance]);
|
displaySet = makeDisplaySet([instance]);
|
||||||
displaySet.setAttributes({
|
displaySet.setAttributes({
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
@ -143,138 +145,6 @@ function getDisplaySetsFromSeries(instances) {
|
|||||||
return displaySets;
|
return displaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove since we have roughly the same thing in dcmjs
|
|
||||||
const sopClassDictionary = {
|
|
||||||
ComputedRadiographyImageStorage: '1.2.840.10008.5.1.4.1.1.1',
|
|
||||||
DigitalXRayImageStorageForPresentation: '1.2.840.10008.5.1.4.1.1.1.1',
|
|
||||||
DigitalXRayImageStorageForProcessing: '1.2.840.10008.5.1.4.1.1.1.1.1',
|
|
||||||
DigitalMammographyXRayImageStorageForPresentation:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.1.2',
|
|
||||||
DigitalMammographyXRayImageStorageForProcessing:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.1.2.1',
|
|
||||||
DigitalIntraOralXRayImageStorageForPresentation:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.1.3',
|
|
||||||
DigitalIntraOralXRayImageStorageForProcessing:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.1.3.1',
|
|
||||||
CTImageStorage: '1.2.840.10008.5.1.4.1.1.2',
|
|
||||||
EnhancedCTImageStorage: '1.2.840.10008.5.1.4.1.1.2.1',
|
|
||||||
LegacyConvertedEnhancedCTImageStorage: '1.2.840.10008.5.1.4.1.1.2.2',
|
|
||||||
UltrasoundMultiframeImageStorage: '1.2.840.10008.5.1.4.1.1.3.1',
|
|
||||||
MRImageStorage: '1.2.840.10008.5.1.4.1.1.4',
|
|
||||||
EnhancedMRImageStorage: '1.2.840.10008.5.1.4.1.1.4.1',
|
|
||||||
MRSpectroscopyStorage: '1.2.840.10008.5.1.4.1.1.4.2',
|
|
||||||
EnhancedMRColorImageStorage: '1.2.840.10008.5.1.4.1.1.4.3',
|
|
||||||
LegacyConvertedEnhancedMRImageStorage: '1.2.840.10008.5.1.4.1.1.4.4',
|
|
||||||
UltrasoundImageStorageRET: '1.2.840.10008.5.1.4.1.1.6',
|
|
||||||
UltrasoundImageStorage: '1.2.840.10008.5.1.4.1.1.6.1',
|
|
||||||
EnhancedUSVolumeStorage: '1.2.840.10008.5.1.4.1.1.6.2',
|
|
||||||
SecondaryCaptureImageStorage: '1.2.840.10008.5.1.4.1.1.7',
|
|
||||||
MultiframeSingleBitSecondaryCaptureImageStorage:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.7.1',
|
|
||||||
MultiframeGrayscaleByteSecondaryCaptureImageStorage:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.7.2',
|
|
||||||
MultiframeGrayscaleWordSecondaryCaptureImageStorage:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.7.3',
|
|
||||||
MultiframeTrueColorSecondaryCaptureImageStorage:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.7.4',
|
|
||||||
Sop12LeadECGWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.1.1',
|
|
||||||
GeneralECGWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.1.2',
|
|
||||||
AmbulatoryECGWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.1.3',
|
|
||||||
HemodynamicWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.2.1',
|
|
||||||
CardiacElectrophysiologyWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.3.1',
|
|
||||||
BasicVoiceAudioWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.4.1',
|
|
||||||
GeneralAudioWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.4.2',
|
|
||||||
ArterialPulseWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.5.1',
|
|
||||||
RespiratoryWaveformStorage: '1.2.840.10008.5.1.4.1.1.9.6.1',
|
|
||||||
GrayscaleSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.1',
|
|
||||||
ColorSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.2',
|
|
||||||
PseudoColorSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.3',
|
|
||||||
BlendingSoftcopyPresentationStateStorage: '1.2.840.10008.5.1.4.1.1.11.4',
|
|
||||||
XAXRFGrayscaleSoftcopyPresentationStateStorage:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.11.5',
|
|
||||||
XRayAngiographicImageStorage: '1.2.840.10008.5.1.4.1.1.12.1',
|
|
||||||
EnhancedXAImageStorage: '1.2.840.10008.5.1.4.1.1.12.1.1',
|
|
||||||
XRayRadiofluoroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.12.2',
|
|
||||||
EnhancedXRFImageStorage: '1.2.840.10008.5.1.4.1.1.12.2.1',
|
|
||||||
XRay3DAngiographicImageStorage: '1.2.840.10008.5.1.4.1.1.13.1.1',
|
|
||||||
XRay3DCraniofacialImageStorage: '1.2.840.10008.5.1.4.1.1.13.1.2',
|
|
||||||
BreastTomosynthesisImageStorage: '1.2.840.10008.5.1.4.1.1.13.1.3',
|
|
||||||
BreastProjectionXRayImageStorageForPresentation:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.13.1.4',
|
|
||||||
BreastProjectionXRayImageStorageForProcessing:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.13.1.5',
|
|
||||||
IntravascularOpticalCoherenceTomographyImageStorageForPresentation:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.14.1',
|
|
||||||
IntravascularOpticalCoherenceTomographyImageStorageForProcessing:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.14.2',
|
|
||||||
NuclearMedicineImageStorage: '1.2.840.10008.5.1.4.1.1.20',
|
|
||||||
RawDataStorage: '1.2.840.10008.5.1.4.1.1.66',
|
|
||||||
SpatialRegistrationStorage: '1.2.840.10008.5.1.4.1.1.66.1',
|
|
||||||
SpatialFiducialsStorage: '1.2.840.10008.5.1.4.1.1.66.2',
|
|
||||||
DeformableSpatialRegistrationStorage: '1.2.840.10008.5.1.4.1.1.66.3',
|
|
||||||
SegmentationStorage: '1.2.840.10008.5.1.4.1.1.66.4',
|
|
||||||
SurfaceSegmentationStorage: '1.2.840.10008.5.1.4.1.1.66.5',
|
|
||||||
RealWorldValueMappingStorage: '1.2.840.10008.5.1.4.1.1.67',
|
|
||||||
SurfaceScanMeshStorage: '1.2.840.10008.5.1.4.1.1.68.1',
|
|
||||||
SurfaceScanPointCloudStorage: '1.2.840.10008.5.1.4.1.1.68.2',
|
|
||||||
VLEndoscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.1',
|
|
||||||
VideoEndoscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.1.1',
|
|
||||||
VLMicroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.2',
|
|
||||||
VideoMicroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.2.1',
|
|
||||||
VLSlideCoordinatesMicroscopicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.3',
|
|
||||||
VLPhotographicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.4',
|
|
||||||
VideoPhotographicImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.4.1',
|
|
||||||
OphthalmicPhotography8BitImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.1',
|
|
||||||
OphthalmicPhotography16BitImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.2',
|
|
||||||
StereometricRelationshipStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.3',
|
|
||||||
OphthalmicTomographyImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.5.4',
|
|
||||||
VLWholeSlideMicroscopyImageStorage: '1.2.840.10008.5.1.4.1.1.77.1.6',
|
|
||||||
LensometryMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.1',
|
|
||||||
AutorefractionMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.2',
|
|
||||||
KeratometryMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.3',
|
|
||||||
SubjectiveRefractionMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.4',
|
|
||||||
VisualAcuityMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.5',
|
|
||||||
SpectaclePrescriptionReportStorage: '1.2.840.10008.5.1.4.1.1.78.6',
|
|
||||||
OphthalmicAxialMeasurementsStorage: '1.2.840.10008.5.1.4.1.1.78.7',
|
|
||||||
IntraocularLensCalculationsStorage: '1.2.840.10008.5.1.4.1.1.78.8',
|
|
||||||
MacularGridThicknessandVolumeReport: '1.2.840.10008.5.1.4.1.1.79.1',
|
|
||||||
OphthalmicVisualFieldStaticPerimetryMeasurementsStorage:
|
|
||||||
'1.2.840.10008.5.1.4.1.1.80.1',
|
|
||||||
OphthalmicThicknessMapStorage: '1.2.840.10008.5.1.4.1.1.81.1',
|
|
||||||
CornealTopographyMapStorage: '1.2.840.10008.5.1.4.1.1.82.1',
|
|
||||||
BasicTextSR: '1.2.840.10008.5.1.4.1.1.88.11',
|
|
||||||
EnhancedSR: '1.2.840.10008.5.1.4.1.1.88.22',
|
|
||||||
ComprehensiveSR: '1.2.840.10008.5.1.4.1.1.88.33',
|
|
||||||
Comprehensive3DSR: '1.2.840.10008.5.1.4.1.1.88.34',
|
|
||||||
ProcedureLog: '1.2.840.10008.5.1.4.1.1.88.40',
|
|
||||||
MammographyCADSR: '1.2.840.10008.5.1.4.1.1.88.50',
|
|
||||||
KeyObjectSelection: '1.2.840.10008.5.1.4.1.1.88.59',
|
|
||||||
ChestCADSR: '1.2.840.10008.5.1.4.1.1.88.65',
|
|
||||||
XRayRadiationDoseSR: '1.2.840.10008.5.1.4.1.1.88.67',
|
|
||||||
RadiopharmaceuticalRadiationDoseSR: '1.2.840.10008.5.1.4.1.1.88.68',
|
|
||||||
ColonCADSR: '1.2.840.10008.5.1.4.1.1.88.69',
|
|
||||||
ImplantationPlanSRDocumentStorage: '1.2.840.10008.5.1.4.1.1.88.70',
|
|
||||||
EncapsulatedPDFStorage: '1.2.840.10008.5.1.4.1.1.104.1',
|
|
||||||
EncapsulatedCDAStorage: '1.2.840.10008.5.1.4.1.1.104.2',
|
|
||||||
PositronEmissionTomographyImageStorage: '1.2.840.10008.5.1.4.1.1.128',
|
|
||||||
EnhancedPETImageStorage: '1.2.840.10008.5.1.4.1.1.130',
|
|
||||||
LegacyConvertedEnhancedPETImageStorage: '1.2.840.10008.5.1.4.1.1.128.1',
|
|
||||||
BasicStructuredDisplayStorage: '1.2.840.10008.5.1.4.1.1.131',
|
|
||||||
RTImageStorage: '1.2.840.10008.5.1.4.1.1.481.1',
|
|
||||||
RTDoseStorage: '1.2.840.10008.5.1.4.1.1.481.2',
|
|
||||||
RTStructureSetStorage: '1.2.840.10008.5.1.4.1.1.481.3',
|
|
||||||
RTBeamsTreatmentRecordStorage: '1.2.840.10008.5.1.4.1.1.481.4',
|
|
||||||
RTPlanStorage: '1.2.840.10008.5.1.4.1.1.481.5',
|
|
||||||
RTBrachyTreatmentRecordStorage: '1.2.840.10008.5.1.4.1.1.481.6',
|
|
||||||
RTTreatmentSummaryRecordStorage: '1.2.840.10008.5.1.4.1.1.481.7',
|
|
||||||
RTIonPlanStorage: '1.2.840.10008.5.1.4.1.1.481.8',
|
|
||||||
RTIonBeamsTreatmentRecordStorage: '1.2.840.10008.5.1.4.1.1.481.9',
|
|
||||||
RTBeamsDeliveryInstructionStorage: '1.2.840.10008.5.1.4.34.7',
|
|
||||||
GenericImplantTemplateStorage: '1.2.840.10008.5.1.4.43.1',
|
|
||||||
ImplantAssemblyTemplateStorage: '1.2.840.10008.5.1.4.44.1',
|
|
||||||
ImplantTemplateGroupStorage: '1.2.840.10008.5.1.4.45.1',
|
|
||||||
};
|
|
||||||
|
|
||||||
const sopClassUids = [
|
const sopClassUids = [
|
||||||
sopClassDictionary.ComputedRadiographyImageStorage,
|
sopClassDictionary.ComputedRadiographyImageStorage,
|
||||||
sopClassDictionary.DigitalXRayImageStorageForPresentation,
|
sopClassDictionary.DigitalXRayImageStorageForPresentation,
|
||||||
@ -325,6 +195,7 @@ const sopClassUids = [
|
|||||||
sopClassDictionary.EnhancedPETImageStorage,
|
sopClassDictionary.EnhancedPETImageStorage,
|
||||||
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
|
sopClassDictionary.LegacyConvertedEnhancedPETImageStorage,
|
||||||
sopClassDictionary.RTImageStorage,
|
sopClassDictionary.RTImageStorage,
|
||||||
|
sopClassDictionary.EnhancedUSVolumeStorage,
|
||||||
];
|
];
|
||||||
|
|
||||||
function getSopClassHandlerModule() {
|
function getSopClassHandlerModule() {
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import getDataSourcesModule from './getDataSourcesModule.js';
|
|||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
import getPanelModule from './getPanelModule';
|
import getPanelModule from './getPanelModule';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||||
import getHangingProtocolModule from './getHangingProtocolModule.js';
|
|
||||||
import getToolbarModule from './getToolbarModule';
|
import getToolbarModule from './getToolbarModule';
|
||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||||
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID';
|
||||||
import { id } from './id.js';
|
import { id } from './id.js';
|
||||||
import init from './init';
|
import init from './init';
|
||||||
@ -18,9 +18,9 @@ const defaultExtension = {
|
|||||||
init({ servicesManager, configuration });
|
init({ servicesManager, configuration });
|
||||||
},
|
},
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
getHangingProtocolModule,
|
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
|
getHangingProtocolModule,
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
getToolbarModule,
|
getToolbarModule,
|
||||||
getCommandsModule({ servicesManager, commandsManager }) {
|
getCommandsModule({ servicesManager, commandsManager }) {
|
||||||
|
|||||||
85
extensions/default/src/utils/getDirectURL.js
Normal file
85
extensions/default/src/utils/getDirectURL.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
DicomMetadataStore,
|
||||||
|
IWebApiDataSource,
|
||||||
|
utils,
|
||||||
|
errorHandler,
|
||||||
|
classes,
|
||||||
|
} from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a URL that can be used for direct retrieve of the bulkdata
|
||||||
|
*
|
||||||
|
* @param {object} params
|
||||||
|
* @param {string} params.tag is the tag name of the URL to retrieve
|
||||||
|
* @param {string} params.defaultPath path for the pixel data url
|
||||||
|
* @param {object} params.instance is the instance object that the tag is in
|
||||||
|
* @param {string} params.defaultType is the mime type of the response
|
||||||
|
* @param {string} params.singlepart is the type of the part to retrieve
|
||||||
|
* @param {string} params.fetchPart unknown?
|
||||||
|
* @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart,
|
||||||
|
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
|
||||||
|
*/
|
||||||
|
const getDirectURL = (wadoRoot, params) => {
|
||||||
|
const {
|
||||||
|
instance,
|
||||||
|
tag = 'PixelData',
|
||||||
|
defaultPath = '/pixeldata',
|
||||||
|
defaultType = 'video/mp4',
|
||||||
|
singlepart = null,
|
||||||
|
singlepart: fetchPart = 'video',
|
||||||
|
} = params;
|
||||||
|
const value = instance[tag];
|
||||||
|
if (!value) return undefined;
|
||||||
|
|
||||||
|
if (value.DirectRetrieveURL) return value.DirectRetrieveURL;
|
||||||
|
if (value.InlineBinary) {
|
||||||
|
const blob = utils.b64toBlob(value.InlineBinary, defaultType);
|
||||||
|
value.DirectRetrieveURL = URL.createObjectURL(blob);
|
||||||
|
return value.DirectRetrieveURL;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!singlepart ||
|
||||||
|
(singlepart !== true && singlepart.indexOf(fetchPart) === -1)
|
||||||
|
) {
|
||||||
|
if (value.retrieveBulkData) {
|
||||||
|
return value.retrieveBulkData().then(arr => {
|
||||||
|
value.DirectRetrieveURL = URL.createObjectURL(
|
||||||
|
new Blob([arr], { type: defaultType })
|
||||||
|
);
|
||||||
|
return value.DirectRetrieveURL;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.warn('Unable to retrieve', tag, 'from', instance);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
} = instance;
|
||||||
|
const BulkDataURI =
|
||||||
|
(value && value.BulkDataURI) ||
|
||||||
|
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
|
||||||
|
const hasQuery = BulkDataURI.indexOf('?') != -1;
|
||||||
|
const hasAccept = BulkDataURI.indexOf('accept=') != -1;
|
||||||
|
const acceptUri =
|
||||||
|
BulkDataURI +
|
||||||
|
(hasAccept ? '' : (hasQuery ? '&' : '?') + `accept=${defaultType}`);
|
||||||
|
if (BulkDataURI.indexOf('http') === 0) return acceptUri;
|
||||||
|
if (BulkDataURI.indexOf('/') === 0) {
|
||||||
|
return wadoRoot + acceptUri;
|
||||||
|
}
|
||||||
|
if (BulkDataURI.indexOf('series/') == 0) {
|
||||||
|
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
|
||||||
|
}
|
||||||
|
if (BulkDataURI.indexOf('instances/') === 0) {
|
||||||
|
return `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/${acceptUri}`;
|
||||||
|
}
|
||||||
|
if (BulkDataURI.indexOf('bulkdata/') === 0) {
|
||||||
|
return `${wadoRoot}/studies/${StudyInstanceUID}/${acceptUri}`;
|
||||||
|
}
|
||||||
|
throw new Error('BulkDataURI in unknown format:' + BulkDataURI);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDirectURL;
|
||||||
@ -30,7 +30,7 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^3.0.0",
|
"@ohif/core": "^3.0.0",
|
||||||
"@ohif/ui": "^2.0.0",
|
"@ohif/ui": "^2.0.0",
|
||||||
"dcmjs": "^0.24.5",
|
"dcmjs": "^0.28.3",
|
||||||
"dicom-parser": "^1.8.9",
|
"dicom-parser": "^1.8.9",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user