Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
383a0ef167 | ||
|
|
56bd49a0cd | ||
|
|
755b283574 | ||
|
|
a8a4fcee40 | ||
|
|
9892320ffb | ||
|
|
28befb1ea6 | ||
|
|
c554c5fac2 | ||
|
|
6e3a58931a | ||
|
|
5beb12a7fc | ||
|
|
1953118c8a | ||
|
|
7807f849f2 | ||
|
|
b426256df5 | ||
|
|
c9b7e97f9c | ||
|
|
0fc7d93d73 | ||
|
|
33307d3cd2 | ||
|
|
c3ce909434 | ||
|
|
ad2c750503 | ||
|
|
cceb61f123 | ||
|
|
13bcba8c39 | ||
|
|
d2db026158 | ||
|
|
e4b0552db4 | ||
|
|
d7ba2fac69 | ||
|
|
761d54fbbf | ||
|
|
79548d2380 | ||
|
|
6a401c9e52 | ||
|
|
23376d279d | ||
|
|
4110b0422c | ||
|
|
ed501bb462 | ||
|
|
abf050e162 | ||
|
|
35ed82e161 | ||
|
|
d0ccc22fd9 | ||
|
|
31841c495f | ||
|
|
65505d9bce | ||
|
|
460fdeb534 | ||
|
|
f4b7091ae4 | ||
|
|
1e72684455 | ||
|
|
5b0b6301b9 | ||
|
|
03a34a75a1 | ||
|
|
729272a985 | ||
|
|
9194ec2404 | ||
|
|
52c95a1209 | ||
|
|
8903adaea7 | ||
|
|
9481e2a99e | ||
|
|
f783fd7650 | ||
|
|
79afee7615 | ||
|
|
539141fe5a | ||
|
|
a046c6c901 | ||
|
|
073be065cf | ||
|
|
252536a1e6 | ||
|
|
53b1d71d56 | ||
|
|
702d99cb93 | ||
|
|
bb7b78bf33 | ||
|
|
b796fc9c62 | ||
|
|
66c587b139 | ||
|
|
f6e2e58aad | ||
|
|
489d4b0584 |
No files matched your search
+11
-7
@@ -25,11 +25,9 @@ executors:
|
||||
docker:
|
||||
# Primary container image where all steps run.
|
||||
- image: 'cypress/browsers:node14.15.0-chrome86-ff82'
|
||||
- image: 'ohif/viewer-testdata:0.1-test'
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: circleci/node:14.15.0
|
||||
- image: cimg/node:14.18
|
||||
environment:
|
||||
TERM: xterm # Enable colors in term
|
||||
QUICK_BUILD: true
|
||||
@@ -119,6 +117,7 @@ jobs:
|
||||
command: yarn install --frozen-lockfile
|
||||
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
docker_layer_caching: false
|
||||
|
||||
- run:
|
||||
@@ -295,6 +294,7 @@ jobs:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build and push Docker image
|
||||
@@ -347,7 +347,8 @@ workflows:
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
git submodule update --init &&
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
|
||||
yarn run build
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
|
||||
@@ -375,7 +376,8 @@ workflows:
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
git submodule update --init &&
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
|
||||
yarn run build:package
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
|
||||
@@ -408,7 +410,8 @@ workflows:
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
git submodule update --init &&
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
|
||||
yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
@@ -487,7 +490,8 @@ workflows:
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
git submodule update --init &&
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/e2e.js
|
||||
yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
custom: https://giving.massgeneral.org/ohif
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: "\U0001F917 Support Question"
|
||||
about: "I have a question \U0001F4AC"
|
||||
title: ''
|
||||
labels: 'Community: Question :question:, Triage :white_flag:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
> :hand: We are a small team with limited resources. Your question is much more
|
||||
> likely to be answered if it is
|
||||
> [a good question](https://stackoverflow.com/help/how-to-ask)
|
||||
|
||||
**Description**
|
||||
|
||||
Questions can often be answered by our documentation. Unable to find an answer
|
||||
in our docs? We'll try to help. In the meantime, if you answer your own
|
||||
question, please respond with the answer here so that others may benefit as
|
||||
well. Better yet, open a PR to expand our docs ^\_^
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🤗 Support Question
|
||||
url: https://community.ohif.org/
|
||||
about: Please use our forum if you have questions or need help.
|
||||
@@ -10,6 +10,9 @@ junit.xml
|
||||
coverage/
|
||||
.docz/
|
||||
*.js.map
|
||||
.yarn
|
||||
platform/docs/.docusaurus
|
||||
platform/ui/storybook-static
|
||||
|
||||
# YALC (for Erik)
|
||||
.yalc
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "testdata"]
|
||||
path = testdata
|
||||
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
|
||||
@@ -0,0 +1 @@
|
||||
14.18.0
|
||||
@@ -28,7 +28,7 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
|
||||
const config = {
|
||||
mode: isProdBuild ? 'production' : 'development',
|
||||
devtool: isProdBuild ? 'source-map' : 'cheap-module-eval-source-map',
|
||||
devtool: isProdBuild ? 'source-map' : 'eval-cheap-module-source-map',
|
||||
entry: {
|
||||
app: `${SRC_DIR}/index.js`,
|
||||
},
|
||||
|
||||
@@ -273,6 +273,17 @@ development of OHIF Viewer:
|
||||
Leidos Biomedical Research under Task Order HHSN26100071 from NCI.
|
||||
[IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized
|
||||
version of the OHIF Viewer.
|
||||
|
||||
### Research notice
|
||||
Please note that this repository is participating in a study into sustainability
|
||||
of open source projects. Data will be gathered about this repository for
|
||||
approximately the next 12 months, starting from June 2021.
|
||||
|
||||
Data collected will include number of contributors, number of PRs, time taken to
|
||||
close/merge these PRs, and issues closed.
|
||||
|
||||
For more information, please visit
|
||||
[our informational page](https://sustainable-open-science-and-software.github.io/) or download our [participant information sheet](https://sustainable-open-science-and-software.github.io/assets/PIS_sustainable_software.pdf).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ OHIF Viewer and its medical imaging data. This post covers one of many potential
|
||||
setups that accomplish that. Please note, noticably absent is user account
|
||||
control.
|
||||
|
||||
Do not use this recipe to host sensitive medical data on the open web. Depending
|
||||
> **Do not use this recipe to host sensitive medical data on the open web!**
|
||||
|
||||
Depending
|
||||
on your company's policies, this may be an appropriate setup on an internal
|
||||
network when protected with a server's basic authentication. For a more robust
|
||||
setup, check out our [user account control recipe](./user-account-control.md)
|
||||
@@ -23,13 +25,13 @@ Our two biggest hurdles when hosting our image archive and web client are:
|
||||
|
||||
### Handling Web Requests
|
||||
|
||||
We mittigate our first issue by allowing [Nginx][nginx] to handle incoming web
|
||||
We mitigate our first issue by allowing [Nginx][nginx] to handle incoming web
|
||||
requests. Nginx is open source software for web serving, reverse proxying,
|
||||
caching, and more. It's designed for maximum performance and stability --
|
||||
allowing us to more reliably serve content than Orthanc's built-in server can.
|
||||
|
||||
More specifically, we accomplish this by using a
|
||||
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
|
||||
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
|
||||
resources from our image archive (Orthanc), and when accessing its web admin.
|
||||
|
||||
> A reverse proxy is a type of proxy server that retrieves resources on behalf
|
||||
@@ -38,7 +40,7 @@ resources from our image archive (Orthanc), and when accessing its web admin.
|
||||
|
||||
### CORS Issues
|
||||
|
||||
Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to
|
||||
[Cross-Origin Resource Sharing][understanding-cors] (CORS) is a mechanism that uses HTTP headers to
|
||||
tell a browser which web applications have permission to access selected
|
||||
resources from a server at a different origin (domain, protocol, port). IE. By
|
||||
default, a Web App located at `http://my-website.com` can't access resources
|
||||
@@ -51,9 +53,6 @@ We can solve this one of two ways:
|
||||
|
||||
**This solution uses the first approach.**
|
||||
|
||||
You can read more about CORS in this Medium article: [Understanding
|
||||
CORS][understanding-cors]
|
||||
|
||||
### Diagram
|
||||
|
||||
This setup allows us to create a setup similar to the one pictured below:
|
||||
@@ -73,6 +72,8 @@ This setup allows us to create a setup similar to the one pictured below:
|
||||
- Docker
|
||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
||||
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
|
||||
- Node 16.x with optional Python and Visual Studio Build Tools ([webpack 4 has an issue](https://github.com/webpack/webpack/issues/14532) with Node 17.x)
|
||||
- [Node.js](https://nodejs.org/en/download/)
|
||||
|
||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
||||
in command prompt or terminal_
|
||||
@@ -81,7 +82,7 @@ in command prompt or terminal_
|
||||
|
||||
_Spin Things Up_
|
||||
|
||||
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
|
||||
- Navigate to `./platform/viewer/.recipes/OpenResty-Orthanc` in your shell
|
||||
- Run `docker-compose up`
|
||||
|
||||
_Upload Your First Study_
|
||||
@@ -94,16 +95,16 @@ _Upload Your First Study_
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
_Exit code 137_
|
||||
#### Exit code 137
|
||||
|
||||
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
|
||||
tab, and increase the amount of Memory available.
|
||||
|
||||
_Cannot create container for service X_
|
||||
#### Cannot create container for service X
|
||||
|
||||
Use this one with caution: `docker system prune`
|
||||
|
||||
_X is already running_
|
||||
#### X is already running
|
||||
|
||||
Stop running all containers:
|
||||
|
||||
@@ -120,15 +121,12 @@ likely want to update:
|
||||
#### OHIF Viewer
|
||||
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
||||
configuration we use is set to a specific file when we build the viewer, and
|
||||
configuration we use is set to [a specific file][config-ohif] when we build the viewer, and
|
||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
||||
in the `dockerfile` for this solution:
|
||||
|
||||
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
||||
|
||||
You can find the configuration we're using here:
|
||||
`/public/config/docker_openresty-orthanc.js`
|
||||
|
||||
To rebuild the `webapp` image created by our `dockerfile` after updating the
|
||||
Viewer's configuration, you can run:
|
||||
|
||||
@@ -137,7 +135,7 @@ Viewer's configuration, you can run:
|
||||
|
||||
#### Other
|
||||
|
||||
All other files are found in: `/docker/OpenResty-Orthanc/`
|
||||
All other files in this directory include
|
||||
|
||||
| Service | Configuration | Docs |
|
||||
| ----------------- | --------------------------------- | ------------------------------------------- |
|
||||
@@ -236,6 +234,8 @@ members put together:
|
||||
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
||||
<!-- SRC -->
|
||||
|
||||
[config-ohif]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/docker_openresty-orthanc.js
|
||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
|
||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
|
||||
|
||||
@@ -3,6 +3,73 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.12.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.6...@ohif/extension-cornerstone@2.12.7) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.5...@ohif/extension-cornerstone@2.12.6) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.4...@ohif/extension-cornerstone@2.12.5) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.3...@ohif/extension-cornerstone@2.12.4) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.2...@ohif/extension-cornerstone@2.12.3) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.1...@ohif/extension-cornerstone@2.12.2) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.0...@ohif/extension-cornerstone@2.12.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.12.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.11.0...@ohif/extension-cornerstone@2.12.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.11.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.10...@ohif/extension-cornerstone@2.11.0) (2021-09-09)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.11.0",
|
||||
"version": "2.12.7",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,12 +32,12 @@
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-math": "^0.1.9",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dcmjs": "0.19.4",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
@@ -51,6 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-cornerstone-viewport": "4.0.5"
|
||||
"react-cornerstone-viewport": "4.1.1"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { setEnabledElement } from './state';
|
||||
import initSRTools from './tools/initSRTools';
|
||||
|
||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||
const {
|
||||
@@ -92,6 +93,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
plugin: 'cornerstone',
|
||||
})
|
||||
);
|
||||
initSRTools(enabledElement);
|
||||
},
|
||||
|
||||
onMeasurementsChanged: (event, action) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import checkForSRAnnotations from './tools/checkForSRAnnotations';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
@@ -187,6 +188,8 @@ class OHIFCornerstoneViewport extends Component {
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
const { viewportIndex } = this.props;
|
||||
checkForSRAnnotations({ displaySet, viewportIndex });
|
||||
this.setStateFromProps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import csTools from 'cornerstone-tools';
|
||||
import merge from 'lodash.merge';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
import dicomSRModule from './tools/modules/dicomSRModule';
|
||||
import srModuleId from './tools/id';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -15,6 +17,8 @@ import measurementServiceMappingsFactory from './utils/measurementServiceMapping
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||
|
||||
csTools.register('module', srModuleId, dicomSRModule);
|
||||
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
if (UIDialogService) {
|
||||
let dialogId = UIDialogService.create({
|
||||
|
||||
@@ -0,0 +1,414 @@
|
||||
import csTools, {
|
||||
importInternal,
|
||||
getToolState,
|
||||
toolColors,
|
||||
} from 'cornerstone-tools';
|
||||
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
/** Internal imports */
|
||||
import TOOL_NAMES from './constants/toolNames';
|
||||
import SCOORD_TYPES from './constants/scoordTypes';
|
||||
import id from './id';
|
||||
|
||||
/** Cornerstone 3rd party dev kit imports */
|
||||
const draw = importInternal('drawing/draw');
|
||||
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||
const drawCircle = importInternal('drawing/drawCircle');
|
||||
const drawEllipse = importInternal('drawing/drawEllipse');
|
||||
const drawHandles = importInternal('drawing/drawHandles');
|
||||
const drawArrow = importInternal('drawing/drawArrow');
|
||||
const getNewContext = importInternal('drawing/getNewContext');
|
||||
const BaseTool = importInternal('base/BaseTool');
|
||||
const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox');
|
||||
|
||||
/**
|
||||
* @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay).
|
||||
*
|
||||
* This is a generic render tool.
|
||||
*
|
||||
* A single tool that, given some schema, can render
|
||||
* POINT, MULTIPOINT, POLYLINE, CIRCLE, and ELLIPSE
|
||||
* value types for a given imageId.
|
||||
*
|
||||
*
|
||||
* @extends cornerstoneTools.BaseTool
|
||||
*/
|
||||
export default class DICOMSRDisplayTool extends BaseTool {
|
||||
constructor(props = {}) {
|
||||
const defaultProps = {
|
||||
mixins: ['enabledOrDisabledBinaryTool'],
|
||||
name: TOOL_NAMES.DICOM_SR_DISPLAY_TOOL,
|
||||
};
|
||||
|
||||
const initialProps = Object.assign(defaultProps, props);
|
||||
|
||||
super(initialProps);
|
||||
|
||||
this._module = csTools.getModule(id);
|
||||
}
|
||||
|
||||
renderToolData(evt) {
|
||||
const eventData = evt.detail;
|
||||
const { element } = eventData;
|
||||
const module = this._module;
|
||||
|
||||
const toolState = getToolState(element, this.name);
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackingUniqueIdentifiersForElement = module.getters.trackingUniqueIdentifiersForElement(
|
||||
element
|
||||
);
|
||||
|
||||
const {
|
||||
activeIndex,
|
||||
trackingUniqueIdentifiers,
|
||||
} = trackingUniqueIdentifiersForElement;
|
||||
|
||||
const activeTrackingUniqueIdentifier =
|
||||
trackingUniqueIdentifiers[activeIndex];
|
||||
|
||||
// Filter toolData to only render the data for the active SR.
|
||||
const filteredToolData = toolState.data.filter(td =>
|
||||
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
|
||||
);
|
||||
|
||||
let shouldRepositionTextBoxes = false;
|
||||
|
||||
for (let i = 0; i < filteredToolData.length; i++) {
|
||||
const data = filteredToolData[i];
|
||||
const { renderableData, labels } = data;
|
||||
|
||||
const color =
|
||||
data.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
? toolColors.getActiveColor()
|
||||
: toolColors.getToolColor();
|
||||
const lineWidth = 2;
|
||||
const options = {
|
||||
color,
|
||||
lineWidth,
|
||||
handleRadius: 6,
|
||||
};
|
||||
|
||||
Object.keys(renderableData).forEach(GraphicType => {
|
||||
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||
|
||||
switch (GraphicType) {
|
||||
case SCOORD_TYPES.POINT:
|
||||
this.renderPoint(renderableDataForGraphicType, eventData, options);
|
||||
break;
|
||||
case SCOORD_TYPES.MULTIPOINT:
|
||||
this.renderMultipoint(
|
||||
renderableDataForGraphicType,
|
||||
eventData,
|
||||
options
|
||||
);
|
||||
break;
|
||||
case SCOORD_TYPES.POLYGON:
|
||||
case SCOORD_TYPES.POLYLINE:
|
||||
this.renderPolyLine(
|
||||
renderableDataForGraphicType,
|
||||
eventData,
|
||||
options
|
||||
);
|
||||
break;
|
||||
case SCOORD_TYPES.CIRCLE:
|
||||
this.renderCircle(renderableDataForGraphicType, eventData, options);
|
||||
break;
|
||||
case SCOORD_TYPES.ELLIPSE:
|
||||
this.renderEllipse(
|
||||
renderableDataForGraphicType,
|
||||
eventData,
|
||||
options
|
||||
);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const { element } = eventData;
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
if (!data.handles || !data.handles.textBox) {
|
||||
const textBox = {
|
||||
active: false,
|
||||
hasMoved: true,
|
||||
movesIndependently: false,
|
||||
drawnIndependently: true,
|
||||
allowedOutsideImage: true,
|
||||
hasBoundingBox: true,
|
||||
};
|
||||
|
||||
const anchorPoints = _getTextBoxAnchorPointsForRenderableData(
|
||||
renderableData,
|
||||
eventData
|
||||
);
|
||||
textBox.anchorPoints = anchorPoints;
|
||||
|
||||
const bottomRight = {
|
||||
x: Math.max(...anchorPoints.map(point => point.x)),
|
||||
y: Math.max(...anchorPoints.map(point => point.y)),
|
||||
};
|
||||
|
||||
textBox.x = bottomRight.x;
|
||||
textBox.y = bottomRight.y;
|
||||
|
||||
data.handles = {};
|
||||
data.handles.textBox = textBox;
|
||||
|
||||
shouldRepositionTextBoxes = true;
|
||||
}
|
||||
|
||||
const text = _getTextBoxLinesFromLabels(labels);
|
||||
|
||||
function textBoxAnchorPoints() {
|
||||
return data.handles.textBox.anchorPoints;
|
||||
}
|
||||
|
||||
draw(context, context => {
|
||||
drawLinkedTextBox(
|
||||
context,
|
||||
element,
|
||||
data.handles.textBox,
|
||||
text,
|
||||
data.handles,
|
||||
textBoxAnchorPoints,
|
||||
color,
|
||||
lineWidth,
|
||||
0,
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// TOOD -> text boxes may overlap with other annotations at the moment.
|
||||
// To be fixed after we get requirements.
|
||||
// if (shouldRepositionTextBoxes) {
|
||||
// this.repositionTextBox(filteredToolData, eventData);
|
||||
// }
|
||||
}
|
||||
|
||||
// repositionTextBox(toolData, eventData) {
|
||||
// const toolBoundingBoxes = [];
|
||||
|
||||
// for (let i = 0; i < toolData.length; i++) {
|
||||
// const toolDataI = toolData[i];
|
||||
|
||||
// const { textBox } = toolDataI.handles;
|
||||
// const { anchorPoints } = textBox;
|
||||
|
||||
// const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints);
|
||||
// // Get the textbox bounding locations.
|
||||
// // Get the tool extents.
|
||||
// }
|
||||
// }
|
||||
|
||||
renderPolyLine(renderableData, eventData, options) {
|
||||
const { element } = eventData;
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
renderableData.forEach(points => {
|
||||
draw(context, context => {
|
||||
drawJoinedLines(context, element, points[0], points, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderMultipoint(renderableData, eventData, options) {
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
renderableData.forEach(points => {
|
||||
draw(context, context => {
|
||||
drawHandles(context, eventData, points, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderPoint(renderableData, eventData, options) {
|
||||
// Render single point as an arrow.
|
||||
const { element, image } = eventData;
|
||||
const { rows, columns } = image;
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
const { color, lineWidth } = options;
|
||||
|
||||
// Find a suitable length for the image size.
|
||||
|
||||
const xOffset = columns / 10;
|
||||
const yOffset = rows / 10;
|
||||
|
||||
renderableData.forEach(points => {
|
||||
const point = points[0]; // The SCOORD type is POINT so the array length is 1.
|
||||
draw(context, context => {
|
||||
// Draw the arrow
|
||||
const handleStartCanvas = cornerstone.pixelToCanvas(element, point);
|
||||
const handleEndCanvas = cornerstone.pixelToCanvas(element, {
|
||||
x: point.x + xOffset,
|
||||
y: point.y + yOffset,
|
||||
});
|
||||
|
||||
drawArrow(
|
||||
context,
|
||||
handleEndCanvas,
|
||||
handleStartCanvas,
|
||||
color,
|
||||
lineWidth,
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderCircle(renderableData, eventData, options) {
|
||||
const { element } = eventData;
|
||||
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
renderableData.forEach(circle => {
|
||||
const { center, radius } = circle;
|
||||
|
||||
drawCircle(context, element, center, radius, options);
|
||||
});
|
||||
}
|
||||
|
||||
renderEllipse(renderableData, eventData, options) {
|
||||
const { element } = eventData;
|
||||
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
renderableData.forEach(ellipse => {
|
||||
const { corner1, corner2 } = ellipse;
|
||||
|
||||
drawEllipse(
|
||||
context,
|
||||
element,
|
||||
corner1,
|
||||
corner2,
|
||||
options,
|
||||
'pixel',
|
||||
0 // TODO -> Work our the initial rotation and add it here so we render appropriately rotated ellipses.
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _getTextBoxLinesFromLabels(labels) {
|
||||
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 3);
|
||||
const lines = [];
|
||||
|
||||
for (let i = 0; i < labelLength; i++) {
|
||||
const labelEntry = labels[i];
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
const SHORT_HAND_MAP = {
|
||||
'Short Axis': 'W ',
|
||||
'Long Axis': 'L ',
|
||||
AREA: 'Area ',
|
||||
Length: '',
|
||||
CORNERSTONEFREETEXT: '',
|
||||
};
|
||||
|
||||
function _labelToShorthand(label) {
|
||||
const shortHand = SHORT_HAND_MAP[label];
|
||||
|
||||
if (shortHand !== undefined) {
|
||||
return shortHand;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) {
|
||||
let anchorPoints = [];
|
||||
|
||||
Object.keys(renderableData).forEach(GraphicType => {
|
||||
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||
|
||||
switch (GraphicType) {
|
||||
case SCOORD_TYPES.POINT:
|
||||
renderableDataForGraphicType.forEach(points => {
|
||||
anchorPoints = [...anchorPoints, ...points];
|
||||
|
||||
// Add other arrow point based on image size.
|
||||
const { image } = eventData;
|
||||
const { rows, columns } = image;
|
||||
|
||||
const xOffset = columns / 10;
|
||||
const yOffset = rows / 10;
|
||||
const point = points[0];
|
||||
|
||||
anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset });
|
||||
});
|
||||
|
||||
break;
|
||||
case SCOORD_TYPES.MULTIPOINT:
|
||||
case SCOORD_TYPES.POLYLINE:
|
||||
case SCOORD_TYPES.POLYGON:
|
||||
renderableDataForGraphicType.forEach(points => {
|
||||
anchorPoints = [...anchorPoints, ...points];
|
||||
});
|
||||
break;
|
||||
case SCOORD_TYPES.CIRCLE:
|
||||
renderableDataForGraphicType.forEach(circle => {
|
||||
const { center, radius } = circle;
|
||||
|
||||
anchorPoints.push({ x: center.x + radius, y: center.y });
|
||||
anchorPoints.push({ x: center.x - radius, y: center.y });
|
||||
anchorPoints.push({ x: center.x, y: center.y + radius });
|
||||
anchorPoints.push({ x: center.x, y: center.y - radius });
|
||||
});
|
||||
|
||||
break;
|
||||
case SCOORD_TYPES.ELLIPSE:
|
||||
renderableDataForGraphicType.forEach(ellipse => {
|
||||
const { corner1, corner2 } = ellipse;
|
||||
|
||||
const halfWidth = Math.abs(corner1.x - corner2.x) / 2;
|
||||
const halfHeight = Math.abs(corner1.y - corner2.y) / 2;
|
||||
|
||||
const center = {
|
||||
x: (corner1.x + corner2.x) / 2,
|
||||
y: (corner1.y + corner2.y) / 2,
|
||||
};
|
||||
|
||||
anchorPoints.push({ x: center.x + halfWidth, y: center.y });
|
||||
anchorPoints.push({ x: center.x - halfWidth, y: center.y });
|
||||
anchorPoints.push({ x: center.x, y: center.y + halfHeight });
|
||||
anchorPoints.push({ x: center.x, y: center.y - halfHeight });
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return anchorPoints;
|
||||
}
|
||||
|
||||
function _getBoundingBoxFromAnchorPoints(anchorPoints) {
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
anchorPoints.forEach(point => {
|
||||
const { x, y } = point;
|
||||
|
||||
if (x > maxX) {
|
||||
maxX = x;
|
||||
} else if (x < minX) {
|
||||
minX = x;
|
||||
}
|
||||
|
||||
if (y > maxX) {
|
||||
maxY = y;
|
||||
} else if (y < minY) {
|
||||
minY = y;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import csTools from 'cornerstone-tools';
|
||||
import cs from 'cornerstone-core';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import { getEnabledElement } from '../state';
|
||||
import id from './id';
|
||||
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
|
||||
const checkForSRAnnotations = ({ viewportIndex, displaySet }) => {
|
||||
const srModule = csTools.getModule(id);
|
||||
|
||||
const element = getEnabledElement(viewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { StudyInstanceUID } = displaySet;
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
if (!studyMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
const srDisplaySets = studyMetadata
|
||||
.getDisplaySets()
|
||||
.filter(ds => ds.Modality === 'SR');
|
||||
if (srDisplaySets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { measurements: _measurements } = srDisplaySets[0];
|
||||
if (!_measurements || _measurements.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const measurements = _measurements.filter(m => m.loaded === true);
|
||||
const measurement = measurements[0];
|
||||
if (!measurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
srModule.setters.trackingUniqueIdentifiersForElement(
|
||||
element,
|
||||
measurements.map(measurement => measurement.TrackingUniqueIdentifier),
|
||||
measurement
|
||||
);
|
||||
|
||||
const { TrackingUniqueIdentifier } = measurement;
|
||||
srModule.setters.activeTrackingUniqueIdentifierForElement(
|
||||
element,
|
||||
TrackingUniqueIdentifier
|
||||
);
|
||||
|
||||
cs.updateImage(element);
|
||||
};
|
||||
|
||||
export default checkForSRAnnotations;
|
||||
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
POINT: 'POINT',
|
||||
MULTIPOINT: 'MULTIPOINT',
|
||||
POLYLINE: 'POLYLINE',
|
||||
CIRCLE: 'CIRCLE',
|
||||
ELLIPSE: 'ELLIPSE',
|
||||
POLYGON: 'POLYGON',
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const TOOL_NAMES = {
|
||||
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
|
||||
};
|
||||
|
||||
export default TOOL_NAMES;
|
||||
@@ -0,0 +1,7 @@
|
||||
const id = 'org.ohif.dicom-sr';
|
||||
|
||||
export default id;
|
||||
|
||||
const SOPClassHandlerName = 'dicom-sr';
|
||||
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||
export { SOPClassHandlerName, SOPClassHandlerId };
|
||||
@@ -0,0 +1,108 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
|
||||
import TOOL_NAMES from './constants/toolNames';
|
||||
import getToolAlias from './utils/getToolAlias';
|
||||
|
||||
/**
|
||||
* Initialize SR cornerstone tools.
|
||||
*
|
||||
* @param {*} targetElement
|
||||
*/
|
||||
const initSRTools = targetElement => {
|
||||
const primaryToolId = 'Wwwc';
|
||||
const toolAlias = getToolAlias(primaryToolId); // These are 1:1 for built-in only
|
||||
|
||||
// ~~ MAGIC
|
||||
cornerstoneTools.addToolForElement(targetElement, DICOMSRDisplayTool);
|
||||
cornerstoneTools.setToolEnabledForElement(
|
||||
targetElement,
|
||||
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL
|
||||
);
|
||||
|
||||
// ~~ Variants
|
||||
cornerstoneTools.addToolForElement(
|
||||
targetElement,
|
||||
cornerstoneTools.LengthTool,
|
||||
{
|
||||
name: 'SRLength',
|
||||
configuration: {
|
||||
renderDashed: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
cornerstoneTools.addToolForElement(
|
||||
targetElement,
|
||||
cornerstoneTools.ArrowAnnotateTool,
|
||||
{
|
||||
name: 'SRArrowAnnotate',
|
||||
configuration: {
|
||||
renderDashed: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
cornerstoneTools.addToolForElement(
|
||||
targetElement,
|
||||
cornerstoneTools.BidirectionalTool,
|
||||
{
|
||||
name: 'SRBidirectional',
|
||||
configuration: {
|
||||
renderDashed: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
cornerstoneTools.addToolForElement(
|
||||
targetElement,
|
||||
cornerstoneTools.EllipticalRoiTool,
|
||||
{
|
||||
name: 'SREllipticalRoi',
|
||||
configuration: {
|
||||
renderDashed: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
cornerstoneTools.addToolForElement(
|
||||
targetElement,
|
||||
cornerstoneTools.RectangleRoiTool,
|
||||
{
|
||||
name: 'SRRectangleRoi',
|
||||
configuration: {
|
||||
renderDashed: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
cornerstoneTools.addToolForElement(
|
||||
targetElement,
|
||||
cornerstoneTools.FreehandRoiTool,
|
||||
{
|
||||
name: 'SRFreehandRoi',
|
||||
configuration: {
|
||||
renderDashed: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ~~ Business as usual
|
||||
cornerstoneTools.setToolActiveForElement(targetElement, 'PanMultiTouch', {
|
||||
pointers: 2,
|
||||
});
|
||||
cornerstoneTools.setToolActiveForElement(targetElement, 'ZoomTouchPinch', {});
|
||||
|
||||
// TODO: Add always dashed tool alternative aliases
|
||||
// TODO: or same name... alternative config?
|
||||
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
|
||||
mouseButtonMask: 1,
|
||||
});
|
||||
cornerstoneTools.setToolActiveForElement(targetElement, 'Pan', {
|
||||
mouseButtonMask: 4,
|
||||
});
|
||||
cornerstoneTools.setToolActiveForElement(targetElement, 'Zoom', {
|
||||
mouseButtonMask: 2,
|
||||
});
|
||||
cornerstoneTools.setToolActiveForElement(
|
||||
targetElement,
|
||||
'StackScrollMouseWheel',
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
export default initSRTools;
|
||||
@@ -0,0 +1,61 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const state = {
|
||||
TrackingUniqueIdentifier: null,
|
||||
trackingIdentifiersByEnabledElementUUID: {},
|
||||
};
|
||||
|
||||
function setTrackingUniqueIdentifiersForElement(
|
||||
element,
|
||||
trackingUniqueIdentifiers,
|
||||
activeIndex = 0
|
||||
) {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const { uuid } = enabledElement;
|
||||
|
||||
state.trackingIdentifiersByEnabledElementUUID[uuid] = {
|
||||
trackingUniqueIdentifiers,
|
||||
activeIndex,
|
||||
};
|
||||
}
|
||||
|
||||
function setActiveTrackingUniqueIdentifierForElement(
|
||||
element,
|
||||
TrackingUniqueIdentifier
|
||||
) {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const { uuid } = enabledElement;
|
||||
|
||||
const trackingIdentifiersForElement =
|
||||
state.trackingIdentifiersByEnabledElementUUID[uuid];
|
||||
|
||||
if (trackingIdentifiersForElement) {
|
||||
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
|
||||
tuid => tuid === TrackingUniqueIdentifier
|
||||
);
|
||||
|
||||
trackingIdentifiersForElement.activeIndex = activeIndex;
|
||||
}
|
||||
}
|
||||
|
||||
function getTrackingUniqueIdentifiersForElement(element) {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const { uuid } = enabledElement;
|
||||
|
||||
if (state.trackingIdentifiersByEnabledElementUUID[uuid]) {
|
||||
return state.trackingIdentifiersByEnabledElementUUID[uuid];
|
||||
}
|
||||
|
||||
return { trackingUniqueIdentifiers: [] };
|
||||
}
|
||||
|
||||
export default {
|
||||
state,
|
||||
getters: {
|
||||
trackingUniqueIdentifiersForElement: getTrackingUniqueIdentifiersForElement,
|
||||
},
|
||||
setters: {
|
||||
trackingUniqueIdentifiersForElement: setTrackingUniqueIdentifiersForElement,
|
||||
activeTrackingUniqueIdentifierForElement: setActiveTrackingUniqueIdentifierForElement,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Get cornerstone tool alias.
|
||||
*
|
||||
* @param {string} toolName
|
||||
* @returns tool alias
|
||||
*/
|
||||
export default function getToolAlias(toolName) {
|
||||
let toolAlias = toolName;
|
||||
|
||||
switch (toolName) {
|
||||
case 'Length':
|
||||
toolAlias = 'SRLength';
|
||||
break;
|
||||
case 'Bidirectional':
|
||||
toolAlias = 'SRBidirectional';
|
||||
break;
|
||||
case 'ArrowAnnotate':
|
||||
toolAlias = 'SRArrowAnnotate';
|
||||
break;
|
||||
case 'EllipticalRoi':
|
||||
toolAlias = 'SREllipticalRoi';
|
||||
break;
|
||||
case 'FreehandRoi':
|
||||
toolAlias = 'SRFreehandRoi';
|
||||
break;
|
||||
case 'RectangleRoi':
|
||||
toolAlias = 'SRRectangleRoi';
|
||||
break;
|
||||
}
|
||||
|
||||
return toolAlias;
|
||||
}
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.1...@ohif/extension-debugging@0.3.2) (2022-03-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.0...@ohif/extension-debugging@0.3.1) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.2...@ohif/extension-debugging@0.3.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.1...@ohif/extension-debugging@0.2.2) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-debugging",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.2",
|
||||
"description": "OHIF extension for debugging.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,12 +28,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
"dicom-parser": "^1.8.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"detect-browser": "5.1.1",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"file-saver": "^2.0.2",
|
||||
"jszip": "^3.2.2"
|
||||
}
|
||||
|
||||
@@ -24,3 +24,21 @@
|
||||
overflow: scroll;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.debug-overflowText {
|
||||
margin: 0 auto;
|
||||
max-width: 300px;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.debug-overflowText-border {
|
||||
margin: 0 auto;
|
||||
max-width: 300px;
|
||||
border: solid 2px #ccc;
|
||||
padding: 12px;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
import { detect } from 'detect-browser';
|
||||
import './DebugReportModal.css';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const DubugReportModal = ({
|
||||
viewports,
|
||||
@@ -70,7 +72,7 @@ const DubugReportModal = ({
|
||||
body += '== Viewport Layout ==\n';
|
||||
body += `Rows\t${numRows}\n`;
|
||||
body += `Columns\t${numColumns}\n\n`;
|
||||
body += '== Viewports ==\n';
|
||||
body += '== SeriesInstanceUIDs ==\n';
|
||||
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
const vsd = viewportSpecificData[viewportIndex];
|
||||
@@ -83,6 +85,40 @@ const DubugReportModal = ({
|
||||
body += `[${row},${column}]\t${vsd.SeriesInstanceUID}\n`;
|
||||
});
|
||||
|
||||
body += '== ReferencedSEGSeriesInstanceUIDs ==\n';
|
||||
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
const displaySet = viewportSpecificData[viewportIndex];
|
||||
const [
|
||||
referencedDisplaySetsRef,
|
||||
referencedDisplaySetsCount,
|
||||
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'SEG');
|
||||
|
||||
const [row, column] = _viewportIndexToViewportPosition(
|
||||
viewportIndex,
|
||||
numColumns
|
||||
);
|
||||
|
||||
body += `[${row},${column}]\t${referencedDisplaySetsRef}\n`;
|
||||
});
|
||||
|
||||
body += '== ReferencedRTSTRUCTSeriesInstanceUIDs ==\n';
|
||||
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
const displaySet = viewportSpecificData[viewportIndex];
|
||||
const [
|
||||
referencedDisplaySetsRef,
|
||||
referencedDisplaySetsCount,
|
||||
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'RTSTRUCT');
|
||||
|
||||
const [row, column] = _viewportIndexToViewportPosition(
|
||||
viewportIndex,
|
||||
numColumns
|
||||
);
|
||||
|
||||
body += `[${row},${column}]\t${referencedDisplaySetsRef}\n`;
|
||||
});
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
@@ -171,7 +207,7 @@ const getCurrentStudyUrl = () => {
|
||||
</tr>
|
||||
<tr>
|
||||
<td>URL</td>
|
||||
<td>{window.location.href}</td>
|
||||
<td className="debug-overflowText">{window.location.href}</td>
|
||||
</tr>
|
||||
</React.Fragment>
|
||||
);
|
||||
@@ -222,6 +258,14 @@ const getLayout = viewports => {
|
||||
<th>SeriesInstanceUIDs</th>
|
||||
</tr>
|
||||
{getSeriesInstanceUIDsPerRow(viewports)}
|
||||
<tr>
|
||||
<th>ReferencedSEGSeriesInstanceUIDs</th>
|
||||
</tr>
|
||||
{getReferencedSEGSeriesInstanceUIDsPerRow(viewports)}
|
||||
<tr>
|
||||
<th>ReferencedRTSTRUCTSeriesInstanceUIDs</th>
|
||||
</tr>
|
||||
{getReferencedRTSTRUCTSeriesInstanceUIDsPerRow(viewports)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -256,6 +300,129 @@ const getBrowserInfo = () => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns DisplaySets that reference the target series, sorted by dateTime
|
||||
*
|
||||
* @param {string} StudyInstanceUID
|
||||
* @param {string} SeriesInstanceUID
|
||||
* @returns Array
|
||||
*/
|
||||
const _getReferencedDisplaysets = (
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
Modality
|
||||
) => {
|
||||
/* Referenced DisplaySets */
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: SeriesInstanceUID,
|
||||
Modality,
|
||||
});
|
||||
|
||||
/* Sort */
|
||||
referencedDisplaysets.sort((a, b) => {
|
||||
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
|
||||
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
|
||||
return bNumber - aNumber;
|
||||
});
|
||||
|
||||
return referencedDisplaysets;
|
||||
};
|
||||
|
||||
const _getReferencedSeriesInstanceUIDsString = (displaySet, Modality) => {
|
||||
const referencedDisplaySets = _getReferencedDisplaysets(
|
||||
displaySet.StudyInstanceUID,
|
||||
displaySet.SeriesInstanceUID,
|
||||
Modality
|
||||
);
|
||||
|
||||
let referencedDisplaySetsRef = 'None';
|
||||
const referencedDisplaySetsCount = referencedDisplaySets.length;
|
||||
if (referencedDisplaySetsCount !== 0) {
|
||||
referencedDisplaySetsRef = '';
|
||||
for (let i = 0; i < referencedDisplaySetsCount - 1; i++) {
|
||||
referencedDisplaySetsRef +=
|
||||
referencedDisplaySets[i].SeriesInstanceUID + ', ';
|
||||
}
|
||||
referencedDisplaySetsRef +=
|
||||
referencedDisplaySets[referencedDisplaySetsCount - 1].SeriesInstanceUID;
|
||||
}
|
||||
|
||||
return [referencedDisplaySetsRef, referencedDisplaySetsCount];
|
||||
};
|
||||
|
||||
const getReferencedSEGSeriesInstanceUIDsPerRow = viewports => {
|
||||
const { viewportSpecificData, numColumns } = viewports;
|
||||
|
||||
// NOTE viewportSpecificData is actually an object with numerical keys.
|
||||
return Object.keys(viewportSpecificData).map(viewportIndex => {
|
||||
const displaySet = viewportSpecificData[viewportIndex];
|
||||
const [
|
||||
referencedDisplaySetsRef,
|
||||
referencedDisplaySetsCount,
|
||||
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'SEG');
|
||||
|
||||
const [row, column] = _viewportIndexToViewportPosition(
|
||||
viewportIndex,
|
||||
numColumns
|
||||
);
|
||||
|
||||
if (referencedDisplaySetsCount > 1) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{`[${row},${column}]`}</td>
|
||||
<td className="debug-overflowText-border">
|
||||
{referencedDisplaySetsRef}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tr>
|
||||
<td>{`[${row},${column}]`}</td>
|
||||
<td className="debug-overflowText">{referencedDisplaySetsRef}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getReferencedRTSTRUCTSeriesInstanceUIDsPerRow = viewports => {
|
||||
const { viewportSpecificData, numColumns } = viewports;
|
||||
|
||||
// NOTE viewportSpecificData is actually an object with numerical keys.
|
||||
return Object.keys(viewportSpecificData).map(viewportIndex => {
|
||||
const displaySet = viewportSpecificData[viewportIndex];
|
||||
const [
|
||||
referencedDisplaySetsRef,
|
||||
referencedDisplaySetsCount,
|
||||
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'RTSTRUCT');
|
||||
|
||||
const [row, column] = _viewportIndexToViewportPosition(
|
||||
viewportIndex,
|
||||
numColumns
|
||||
);
|
||||
|
||||
if (referencedDisplaySetsCount > 1) {
|
||||
return (
|
||||
<tr>
|
||||
<td>{`[${row},${column}]`}</td>
|
||||
<td className="debug-overflowText-border">
|
||||
{referencedDisplaySetsRef}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<tr>
|
||||
<td>{`[${row},${column}]`}</td>
|
||||
<td className="debug-overflowText">{referencedDisplaySetsRef}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getSeriesInstanceUIDsPerRow = viewports => {
|
||||
const { viewportSpecificData, numColumns } = viewports;
|
||||
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.17...@ohif/extension-dicom-html@1.3.18) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.16...@ohif/extension-dicom-html@1.3.17) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.15...@ohif/extension-dicom-html@1.3.16) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.14...@ohif/extension-dicom-html@1.3.15) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.13...@ohif/extension-dicom-html@1.3.14) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.12...@ohif/extension-dicom-html@1.3.13) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.11...@ohif/extension-dicom-html@1.3.12) (2021-10-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.10...@ohif/extension-dicom-html@1.3.11) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.9...@ohif/extension-dicom-html@1.3.10) (2021-06-02)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.18",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dcmjs": "0.19.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -160,7 +160,7 @@ const getContentSequence = (data, level = 1) => {
|
||||
|
||||
const root = [];
|
||||
if (header) {
|
||||
const HeaderDynamicLevel = `h${level}`;
|
||||
const HeaderDynamicLevel = `h${Math.min(level, 6)}`;
|
||||
|
||||
root.push(<HeaderDynamicLevel key={header}>{header}</HeaderDynamicLevel>);
|
||||
}
|
||||
@@ -170,7 +170,7 @@ const getContentSequence = (data, level = 1) => {
|
||||
|
||||
if (key === '_meta') {
|
||||
const HeaderDynamicLevel = `h3`;
|
||||
root.push(<hr />);
|
||||
root.push(<hr key={root.length} />);
|
||||
root.push(
|
||||
<HeaderDynamicLevel key="Metadata">
|
||||
DICOM File Meta Information
|
||||
@@ -217,7 +217,7 @@ class DicomHtmlViewport extends Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const dataSet = this.setContentFromByteArray(this.props.byteArray);
|
||||
this.setContentFromByteArray(this.props.byteArray);
|
||||
}
|
||||
|
||||
setContentFromByteArray(byteArray) {
|
||||
|
||||
@@ -5,11 +5,12 @@ const SOP_CLASS_UIDS = {
|
||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||
COMPREHENSIVE_3D_SR: '1.2.840.10008.5.1.4.1.1.88.34',
|
||||
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
|
||||
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
||||
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71',
|
||||
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71'
|
||||
};
|
||||
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
@@ -46,6 +47,8 @@ const OHIFDicomHtmlSopClassHandler = {
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
authorizationHeaders,
|
||||
sopClassUids: sopClassUIDs,
|
||||
images: series._instances,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.1.1...@ohif/extension-dicom-pdf@1.1.2) (2022-01-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.1.0...@ohif/extension-dicom-pdf@1.1.1) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.6...@ohif/extension-dicom-pdf@1.1.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.5...@ohif/extension-dicom-pdf@1.0.6) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.6",
|
||||
"version": "1.1.2",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -33,6 +33,7 @@ class DicomPDFViewport extends Component {
|
||||
|
||||
static propTypes = {
|
||||
byteArray: TypedArrayProp.uint8,
|
||||
rawPdf: PropTypes.bool,
|
||||
useNative: PropTypes.bool,
|
||||
viewportData: PropTypes.object,
|
||||
activeViewportIndex: PropTypes.number,
|
||||
@@ -45,7 +46,8 @@ class DicomPDFViewport extends Component {
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const dataSet = this.parseByteArray(this.props.byteArray);
|
||||
const { rawPdf } = this.props
|
||||
const dataSet = !rawPdf && this.parseByteArray(this.props.byteArray);
|
||||
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
|
||||
|
||||
this.setState(state => ({ ...state, fileURL }));
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import OHIF from '@ohif/core';
|
||||
import OHIFComponentPlugin from './OHIFComponentPlugin.js';
|
||||
import DicomPDFViewport from './DicomPDFViewport';
|
||||
import { str2ab } from '@ohif/core';
|
||||
|
||||
const { DicomLoaderService } = OHIF.utils;
|
||||
|
||||
@@ -18,6 +19,7 @@ class OHIFDicomPDFViewport extends Component {
|
||||
|
||||
state = {
|
||||
byteArray: null,
|
||||
rawPdf: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@@ -34,6 +36,15 @@ class OHIFDicomPDFViewport extends Component {
|
||||
componentDidMount() {
|
||||
const { displaySet, studies } = this.props.viewportData;
|
||||
|
||||
if (displaySet.metadata && displaySet.metadata.EncapsulatedDocument) {
|
||||
const { InlineBinary, BulkDataURI } = displaySet.metadata.EncapsulatedDocument;
|
||||
if (InlineBinary) {
|
||||
const inlineBinaryData = atob(InlineBinary);
|
||||
const byteArray = str2ab(inlineBinaryData);
|
||||
this.setState({ byteArray, rawPdf: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
|
||||
data => this.setState({ byteArray: new Uint8Array(data) }),
|
||||
error => {
|
||||
@@ -49,15 +60,16 @@ class OHIFDicomPDFViewport extends Component {
|
||||
viewportIndex,
|
||||
activeViewportIndex,
|
||||
} = this.props;
|
||||
const { byteArray, error } = this.state;
|
||||
const { byteArray, error, rawPdf } = this.state;
|
||||
const { id, init, destroy } = OHIFDicomPDFViewport;
|
||||
const pluginProps = { id, init, destroy };
|
||||
|
||||
return (
|
||||
<OHIFComponentPlugin {...pluginProps}>
|
||||
{byteArray && (
|
||||
{(byteArray) && (
|
||||
<DicomPDFViewport
|
||||
byteArray={byteArray}
|
||||
rawPdf={rawPdf}
|
||||
setViewportActive={setViewportActive}
|
||||
viewportIndex={viewportIndex}
|
||||
activeViewportIndex={activeViewportIndex}
|
||||
|
||||
@@ -3,6 +3,81 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.7...@ohif/extension-dicom-rt@0.7.8) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.6...@ohif/extension-dicom-rt@0.7.7) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.5...@ohif/extension-dicom-rt@0.7.6) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.4...@ohif/extension-dicom-rt@0.7.5) (2022-01-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.3...@ohif/extension-dicom-rt@0.7.4) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.2...@ohif/extension-dicom-rt@0.7.3) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.1...@ohif/extension-dicom-rt@0.7.2) (2021-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.0...@ohif/extension-dicom-rt@0.7.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.14...@ohif/extension-dicom-rt@0.7.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.13...@ohif/extension-dicom-rt@0.6.14) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-rt",
|
||||
"version": "0.6.14",
|
||||
"version": "0.7.8",
|
||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,9 +29,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.18.8",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"dcmjs": "0.19.4",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
|
||||
@@ -81,13 +81,10 @@ const RTPanel = ({
|
||||
useEffect(() => {
|
||||
updateStructureSets();
|
||||
|
||||
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
document.addEventListener('rt-panel-tab-updated', updateStructureSets);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
'extensiondicomrtrtloaded',
|
||||
updateStructureSets
|
||||
);
|
||||
document.removeEventListener('rt-panel-tab-updated', updateStructureSets);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -91,17 +91,12 @@ export default {
|
||||
}
|
||||
|
||||
if (activeViewport) {
|
||||
const study = studies.find(
|
||||
s => s.StudyInstanceUID === activeViewport.StudyInstanceUID
|
||||
);
|
||||
const ds = study.displaySets.find(
|
||||
ds =>
|
||||
ds.displaySetInstanceUID ===
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
if (!studyMetadata) {
|
||||
return;
|
||||
}
|
||||
const referencedDisplaySets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
@@ -112,6 +107,10 @@ export default {
|
||||
['RTSTRUCT'].includes(ds.Modality)
|
||||
)
|
||||
) {
|
||||
triggerRTPanelUpdatedEvent({
|
||||
badgeNumber: referencedDisplaySets.length,
|
||||
target: 'rt-panel',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,81 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.7...@ohif/extension-dicom-segmentation@0.7.8) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.6...@ohif/extension-dicom-segmentation@0.7.7) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.5...@ohif/extension-dicom-segmentation@0.7.6) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.4...@ohif/extension-dicom-segmentation@0.7.5) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.3...@ohif/extension-dicom-segmentation@0.7.4) (2021-10-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.2...@ohif/extension-dicom-segmentation@0.7.3) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.1...@ohif/extension-dicom-segmentation@0.7.2) (2021-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.0...@ohif/extension-dicom-segmentation@0.7.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.11...@ohif/extension-dicom-segmentation@0.7.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.10...@ohif/extension-dicom-segmentation@0.6.11) (2021-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.6.11",
|
||||
"version": "0.7.8",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,9 +29,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.18.8",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"dcmjs": "0.19.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -24,6 +24,7 @@ const SegmentItem = ({
|
||||
onClick,
|
||||
itemClass,
|
||||
color,
|
||||
labelmap3D,
|
||||
visible,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
@@ -39,7 +40,7 @@ const SegmentItem = ({
|
||||
event.stopPropagation();
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility, index);
|
||||
onVisibilityChange(newVisibility, index, labelmap3D);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+36
-15
@@ -431,23 +431,43 @@ const SegmentationPanel = ({
|
||||
return enabledElements[activeIndex].element;
|
||||
};
|
||||
|
||||
const onSegmentVisibilityChangeHandler = (isVisible, segmentNumber) => {
|
||||
/** Get all labelmaps with this segmentNumber (overlapping segments) */
|
||||
const { labelmaps3D } = getBrushStackState();
|
||||
const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => {
|
||||
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
|
||||
segmentsOnLabelmap.includes(segmentNumber)
|
||||
);
|
||||
});
|
||||
|
||||
const onSegmentVisibilityChangeHandler = (
|
||||
isVisible,
|
||||
segmentNumber,
|
||||
labelmap3D
|
||||
) => {
|
||||
let segmentsHidden = [];
|
||||
possibleLabelMaps3D.forEach(labelmap3D => {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
if (labelmap3D.metadata.hasOverlapping) {
|
||||
/** Get all labelmaps with this segmentNumber and that
|
||||
* are from the same series (overlapping segments) */
|
||||
const { labelmaps3D } = getBrushStackState();
|
||||
|
||||
segmentsHidden = [
|
||||
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
|
||||
];
|
||||
});
|
||||
const sameSeriesLabelMaps3D = labelmaps3D.filter(({ metadata }) => {
|
||||
return (
|
||||
labelmap3D.metadata.segmentationSeriesInstanceUID ===
|
||||
metadata.segmentationSeriesInstanceUID
|
||||
);
|
||||
});
|
||||
|
||||
const possibleLabelMaps3D = sameSeriesLabelMaps3D.filter(
|
||||
({ labelmaps2D }) => {
|
||||
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
|
||||
segmentsOnLabelmap.includes(segmentNumber)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
possibleLabelMaps3D.forEach(labelmap3D => {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
|
||||
segmentsHidden = [
|
||||
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
|
||||
];
|
||||
});
|
||||
} else {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
segmentsHidden = [...labelmap3D.segmentsHidden];
|
||||
}
|
||||
|
||||
setState(state => ({ ...state, segmentsHidden }));
|
||||
|
||||
@@ -514,6 +534,7 @@ const SegmentationPanel = ({
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
labelmap3D={labelmap3D}
|
||||
visible={!labelmap3D.segmentsHidden[segmentIndex]}
|
||||
onVisibilityChange={onSegmentVisibilityChangeHandler}
|
||||
/>
|
||||
|
||||
@@ -64,8 +64,17 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
metadata,
|
||||
};
|
||||
|
||||
segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
|
||||
segDisplaySet.getSourceDisplaySet = function(
|
||||
studies,
|
||||
activateLabelMap = true,
|
||||
onDisplaySetLoadFailureHandler
|
||||
) {
|
||||
return getSourceDisplaySet(
|
||||
studies,
|
||||
segDisplaySet,
|
||||
activateLabelMap,
|
||||
onDisplaySetLoadFailureHandler
|
||||
);
|
||||
};
|
||||
|
||||
segDisplaySet.load = async function(referencedDisplaySet, studies) {
|
||||
@@ -98,6 +107,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
if (labelmapBufferArray.length > 1) {
|
||||
let labelmapIndexes = [];
|
||||
for (let i = 0; i < labelmapBufferArray.length; ++i) {
|
||||
segMetadata.segmentationSeriesInstanceUID =
|
||||
segDisplaySet.SeriesInstanceUID;
|
||||
segMetadata.hasOverlapping = true;
|
||||
labelmapIndexes.push(
|
||||
await loadSegmentation(
|
||||
imageIds,
|
||||
@@ -118,6 +130,9 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
labelmapIndex = labelmapIndexes[0];
|
||||
console.warn('Overlapping segments!');
|
||||
} else {
|
||||
segMetadata.segmentationSeriesInstanceUID =
|
||||
segDisplaySet.SeriesInstanceUID;
|
||||
segMetadata.hasOverlapping = false;
|
||||
labelmapIndex = await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
@@ -135,10 +150,16 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
}
|
||||
|
||||
function _parseSeg(arrayBuffer, imageIds) {
|
||||
const skipOverlapping = false;
|
||||
const tolerance = 1e-2;
|
||||
const cornerstoneToolsVersion = 4;
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
cornerstone.metaData
|
||||
cornerstone.metaData,
|
||||
skipOverlapping,
|
||||
tolerance,
|
||||
cornerstoneToolsVersion
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ export default {
|
||||
label: 'Segmentations',
|
||||
target: 'segmentation-panel',
|
||||
stateEvent: SegmentationPanelTabUpdatedEvent,
|
||||
isDisabled: studies => {
|
||||
isDisabled: (studies, activeViewport) => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
}
|
||||
@@ -139,6 +139,23 @@ export default {
|
||||
const series = study.series[j];
|
||||
|
||||
if (series.Modality === 'SEG') {
|
||||
if (activeViewport) {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
if (!studyMetadata) {
|
||||
return;
|
||||
}
|
||||
const referencedDS = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID:
|
||||
activeViewport.SeriesInstanceUID,
|
||||
Modality: 'SEG',
|
||||
});
|
||||
triggerSegmentationPanelTabUpdatedEvent({
|
||||
badgeNumber: referencedDS.length,
|
||||
target: 'segmentation-panel',
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,70 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.18...@ohif/extension-dicom-tag-browser@0.2.19) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.17...@ohif/extension-dicom-tag-browser@0.2.18) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.16...@ohif/extension-dicom-tag-browser@0.2.17) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.15...@ohif/extension-dicom-tag-browser@0.2.16) (2021-12-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.14...@ohif/extension-dicom-tag-browser@0.2.15) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.13...@ohif/extension-dicom-tag-browser@0.2.14) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.12...@ohif/extension-dicom-tag-browser@0.2.13) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.11...@ohif/extension-dicom-tag-browser@0.2.12) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.10...@ohif/extension-dicom-tag-browser@0.2.11) (2021-06-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-tag-browser",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.19",
|
||||
"description": "OHIF extension for checking DICOM headers.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,12 +28,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dcmjs": "0.19.4",
|
||||
"react": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"moment": "2.24.0",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
activeDisplaySetInstanceUID,
|
||||
setActiveDisplaySetInstanceUID,
|
||||
] = useState(displaySetInstanceUID);
|
||||
const [activeInstance, setActiveInstance] = useState(0);
|
||||
const [activeInstance, setActiveInstance] = useState(1);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [meta, setMeta] = useState('');
|
||||
const [instanceList, setInstanceList] = useState([]);
|
||||
@@ -51,20 +51,21 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
description: displayDate,
|
||||
onClick: () => {
|
||||
setActiveDisplaySetInstanceUID(displaySetInstanceUID);
|
||||
setActiveInstance(0);
|
||||
setActiveInstance(1);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let metadata;
|
||||
const isImageStack = activeDisplaySet instanceof ImageSet;
|
||||
const isImageStack =
|
||||
activeDisplaySet instanceof ImageSet &&
|
||||
activeDisplaySet.isModalitySupported === true;
|
||||
|
||||
let selectedInstanceValue;
|
||||
let instanceList;
|
||||
|
||||
if (isImageStack) {
|
||||
const { images } = activeDisplaySet;
|
||||
const image = images[activeInstance];
|
||||
const image = images[activeInstance - 1];
|
||||
|
||||
instanceList = images.map((image, index) => {
|
||||
const metadata = image.getData().metadata;
|
||||
@@ -91,7 +92,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
setInstanceList(instanceList);
|
||||
setDisplaySetList(newDisplaySetList);
|
||||
setIsImageStack(isImageStack);
|
||||
}, [activeDisplaySetInstanceUID, activeInstance]);
|
||||
}, [activeDisplaySetInstanceUID, activeInstance, displaySets]);
|
||||
|
||||
const selectedDisplaySetValue = displaySetList.find(
|
||||
ds => ds.value === activeDisplaySetInstanceUID
|
||||
@@ -106,7 +107,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
showValue
|
||||
step={1}
|
||||
min={1}
|
||||
max={instanceList.length - 1}
|
||||
max={instanceList.length}
|
||||
value={activeInstance}
|
||||
valueRenderer={value => <p>Instance Number: {value}</p>}
|
||||
onChange={({ target }) => {
|
||||
|
||||
@@ -3,6 +3,145 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.15...@ohif/extension-vtk@1.12.16) (2022-02-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.14...@ohif/extension-vtk@1.12.15) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.13...@ohif/extension-vtk@1.12.14) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.12...@ohif/extension-vtk@1.12.13) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.11...@ohif/extension-vtk@1.12.12) (2022-01-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.10...@ohif/extension-vtk@1.12.11) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.9...@ohif/extension-vtk@1.12.10) (2022-01-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.8...@ohif/extension-vtk@1.12.9) (2021-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.7...@ohif/extension-vtk@1.12.8) (2021-12-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.6...@ohif/extension-vtk@1.12.7) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.5...@ohif/extension-vtk@1.12.6) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.4...@ohif/extension-vtk@1.12.5) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.3...@ohif/extension-vtk@1.12.4) (2021-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.2...@ohif/extension-vtk@1.12.3) (2021-10-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.1...@ohif/extension-vtk@1.12.2) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.12.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.0...@ohif/extension-vtk@1.12.1) (2021-09-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.18...@ohif/extension-vtk@1.12.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.17...@ohif/extension-vtk@1.11.18) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.11.18",
|
||||
"version": "1.12.16",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,11 +32,11 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/i18n": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dcmjs": "0.19.4",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
@@ -50,14 +50,14 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.14.2"
|
||||
"react-vtkjs-viewport": "^0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.14.1",
|
||||
"@ohif/ui": "^1.10.1",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"@ohif/core": "^2.16.12",
|
||||
"@ohif/ui": "^1.10.6",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dicom-parser": "^1.8.11",
|
||||
"gh-pages": "^2.0.1",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
0 silly argv {
|
||||
0 silly argv _: [ 'run' ],
|
||||
0 silly argv stream: true,
|
||||
0 silly argv lernaVersion: '3.18.3',
|
||||
0 silly argv '$0': 'node_modules/.bin/lerna',
|
||||
0 silly argv script: 'dev:viewer'
|
||||
0 silly argv }
|
||||
1 notice cli v3.18.3
|
||||
2 verbose rootPath /home/davide/Development/OIHF/Viewers
|
||||
3 info versioning independent
|
||||
4 info Executing command in 1 package: "yarn run dev:viewer"
|
||||
5 silly npmRunScript.stream [ 'dev:viewer', [], '@ohif/viewer' ]
|
||||
6 silly getExecOpts /home/davide/Development/OIHF/Viewers/platform/viewer undefined
|
||||
7 error MaxBufferError: stderr maxBuffer exceeded
|
||||
7 error at PassThrough.<anonymous> (/home/davide/Development/OIHF/Viewers/node_modules/get-stream/index.js:41:19)
|
||||
7 error at PassThrough.emit (events.js:412:35)
|
||||
7 error at addChunk (internal/streams/readable.js:290:12)
|
||||
7 error at readableAddChunk (internal/streams/readable.js:261:11)
|
||||
7 error at PassThrough.Readable.push (internal/streams/readable.js:204:10)
|
||||
7 error at PassThrough.Transform.push (internal/streams/transform.js:166:32)
|
||||
7 error at PassThrough.afterTransform (internal/streams/transform.js:101:10)
|
||||
7 error at PassThrough._transform (internal/streams/passthrough.js:46:3)
|
||||
7 error at PassThrough.Transform._read (internal/streams/transform.js:205:10)
|
||||
+4
-2
@@ -56,6 +56,7 @@
|
||||
"@babel/preset-env": "^7.5.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@percy/cypress": "^2.2.0",
|
||||
"@webpack-cli/serve": "^1.6.1",
|
||||
"babel-eslint": "9.x",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-inline-react-svg": "1.1.0",
|
||||
@@ -104,8 +105,8 @@
|
||||
"stylus-loader": "^3.0.2",
|
||||
"terser-webpack-plugin": "^2.1.0",
|
||||
"webpack": "^4.35.2",
|
||||
"webpack-cli": "^3.3.5",
|
||||
"webpack-dev-server": "^3.7.2",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.3",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^4.2.1",
|
||||
"workbox-webpack-plugin": "^5.0.0-beta.1",
|
||||
@@ -123,6 +124,7 @@
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"**/node-gyp": "8.4.1",
|
||||
"**/@babel/runtime": "7.5.5"
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,138 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.16.12](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.11...@ohif/core@2.16.12) (2022-02-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.10...@ohif/core@2.16.11) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.9...@ohif/core@2.16.10) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.8...@ohif/core@2.16.9) (2022-02-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.7...@ohif/core@2.16.8) (2022-01-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.6...@ohif/core@2.16.7) (2022-01-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.5...@ohif/core@2.16.6) (2022-01-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.4...@ohif/core@2.16.5) (2021-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DICOMFileLoadingListener typo in method name ([#2647](https://github.com/OHIF/Viewers/issues/2647)) ([761d54f](https://github.com/OHIF/Viewers/commit/761d54fbbfb376c20ee15794ef7bb7111f64a06f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.3...@ohif/core@2.16.4) (2021-12-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* SeriesTime and SeriesDate are only set for the first series and undefined for the remaining ones ([#2476](https://github.com/OHIF/Viewers/issues/2476)) ([6a401c9](https://github.com/OHIF/Viewers/commit/6a401c9e5204cb4f7af6aa33190ad5a8d0879de6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.2...@ohif/core@2.16.3) (2021-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.1...@ohif/core@2.16.2) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.16.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.0...@ohif/core@2.16.1) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.16.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.15.1...@ohif/core@2.16.0) (2021-10-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Added support for WADO servers built with static hosting (e.g. S3) ([#2566](https://github.com/OHIF/Viewers/issues/2566)) ([a046c6c](https://github.com/OHIF/Viewers/commit/a046c6c9017a84e5ead71d5113b5e892333b4130))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.15.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.15.0...@ohif/core@2.15.1) (2021-09-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.15.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.14.1...@ohif/core@2.15.0) (2021-09-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update to WADO Image Loader 4.0.3 to add dynamically imported WebAssembly codec support 🚀 ([#2537](https://github.com/OHIF/Viewers/issues/2537)) ([489d4b0](https://github.com/OHIF/Viewers/commit/489d4b0584f4657ceda1c5b9b68d5e9733827e47))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.14.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.14.0...@ohif/core@2.14.1) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.14.1",
|
||||
"version": "2.16.12",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -31,20 +31,21 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
"cornerstone-core": "^2.6.1",
|
||||
"cornerstone-tools": "^6.0.6",
|
||||
"cornerstone-wado-image-loader": "^4.1.0",
|
||||
"dicom-parser": "^1.8.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "0.18.8",
|
||||
"dicomweb-client": "^0.8.1",
|
||||
"immer": "6.0.2",
|
||||
"dcmjs": "0.19.4",
|
||||
"dicomweb-client": "^0.8.3",
|
||||
"immer": "9.0.12",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
"mathjs": "^10.1.0",
|
||||
"mousetrap": "^1.6.3",
|
||||
"retry": "^0.12.0",
|
||||
"validate.js": "^0.12.0"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export default {
|
||||
POINT: 'POINT',
|
||||
MULTIPOINT: 'MULTIPOINT',
|
||||
POLYLINE: 'POLYLINE',
|
||||
CIRCLE: 'CIRCLE',
|
||||
ELLIPSE: 'ELLIPSE',
|
||||
POLYGON: 'POLYGON',
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const TOOL_NAMES = {
|
||||
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
|
||||
};
|
||||
|
||||
export default TOOL_NAMES;
|
||||
@@ -0,0 +1,29 @@
|
||||
export const CodeNameCodeSequenceValues = {
|
||||
ImagingMeasurementReport: '126000',
|
||||
ImageLibrary: '111028',
|
||||
ImagingMeasurements: '126010',
|
||||
MeasurementGroup: '125007',
|
||||
ImageLibraryGroup: '126200',
|
||||
TrackingUniqueIdentifier: '112040',
|
||||
TrackingIdentifier: '112039',
|
||||
Finding: '121071',
|
||||
FindingSite: 'G-C0E3', // SRT
|
||||
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
|
||||
Score: '246262008',
|
||||
};
|
||||
|
||||
export const RELATIONSHIP_TYPE = {
|
||||
INFERRED_FROM: 'INFERRED FROM',
|
||||
SELECTED_FROM: 'SELECTED FROM',
|
||||
};
|
||||
|
||||
export const CodingSchemeDesignators = {
|
||||
SRT: 'SRT',
|
||||
cornerstoneTools4: 'CST4',
|
||||
};
|
||||
|
||||
export default {
|
||||
CodeNameCodeSequenceValues,
|
||||
RELATIONSHIP_TYPE,
|
||||
CodingSchemeDesignators,
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { ImageSet } from '../../classes';
|
||||
import getMeasurements from './utils/getMeasurements';
|
||||
import getReferencedImagesList from './utils/getReferencedImagesList';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import addMeasurement from './utils/addMeasurement';
|
||||
|
||||
const parseSCOORD3D = ({ servicesManager, displaySets }) => {
|
||||
const { MeasurementService } = servicesManager.services;
|
||||
|
||||
const srDisplaySets = displaySets.filter(ds => ds.Modality === 'SR');
|
||||
|
||||
srDisplaySets.forEach(srDisplaySet => {
|
||||
const firstInstance = srDisplaySet.metadata;
|
||||
if (!firstInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { ContentSequence } = firstInstance;
|
||||
|
||||
srDisplaySet.referencedImages = getReferencedImagesList(ContentSequence);
|
||||
srDisplaySet.measurements = getMeasurements(ContentSequence, srDisplaySet);
|
||||
const mappings = MeasurementService.getSourceMappings(
|
||||
'CornerstoneTools',
|
||||
'4'
|
||||
);
|
||||
|
||||
srDisplaySet.isHydrated = false;
|
||||
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
|
||||
srDisplaySet.isLoaded = true;
|
||||
|
||||
const imageDisplaySets = displaySets.filter(
|
||||
ds =>
|
||||
ds.Modality !== 'SR' &&
|
||||
ds.Modality !== 'SEG' &&
|
||||
ds.Modality !== 'RTSTRUCT' &&
|
||||
ds.Modality !== 'RTDOSE'
|
||||
);
|
||||
imageDisplaySets.forEach(imageDisplaySet => {
|
||||
// Check currently added displaySets and add measurements if the sources exist.
|
||||
checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, imageDisplaySet);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkIfCanAddMeasurementsToDisplaySet = (
|
||||
srDisplaySet,
|
||||
imageDisplaySet
|
||||
) => {
|
||||
let measurements = srDisplaySet.measurements;
|
||||
|
||||
/**
|
||||
* Look for image sets.
|
||||
* This also filters out _this_ displaySet, as it is not an image set.
|
||||
*/
|
||||
if (!(imageDisplaySet instanceof ImageSet)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sopClassUIDs, images } = imageDisplaySet;
|
||||
|
||||
/**
|
||||
* Filter measurements that references the correct sop class.
|
||||
*/
|
||||
measurements = measurements.filter(measurement => {
|
||||
return measurement.coords.some(coord => {
|
||||
if (coord.ReferencedSOPSequence === undefined) {
|
||||
/** we miss the referenced information. We can compare the annotation SCOORD3D coordinates with
|
||||
* the ImagePatientPosition of the frames. However (WARNING!!!),
|
||||
* if more than a source series is present, this logic can find the wrong frame
|
||||
* (i.e. two source series, with the same frameOfReferenceUID,
|
||||
* that have each a frame with the same ImagePositionPatient of the annotation 3D coordinates)
|
||||
*/
|
||||
for (let i = 0; i < images.length; ++i) {
|
||||
const imageMetadata = images[i].getData().metadata;
|
||||
if (
|
||||
imageMetadata.FrameOfReferenceUID !==
|
||||
coord.ReferencedFrameOfReferenceSequence
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sliceNormal = [0, 0, 0];
|
||||
const orientation = imageMetadata.ImageOrientationPatient;
|
||||
sliceNormal[0] =
|
||||
orientation[1] * orientation[5] - orientation[2] * orientation[4];
|
||||
sliceNormal[1] =
|
||||
orientation[2] * orientation[3] - orientation[0] * orientation[5];
|
||||
sliceNormal[2] =
|
||||
orientation[0] * orientation[4] - orientation[1] * orientation[3];
|
||||
|
||||
let distanceAlongNormal = 0;
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
distanceAlongNormal +=
|
||||
sliceNormal[j] * imageMetadata.ImagePositionPatient[j];
|
||||
}
|
||||
|
||||
// assuming 1 mm tolerance
|
||||
if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
coord.ReferencedSOPSequence = {
|
||||
ReferencedSOPClassUID: imageMetadata.SOPClassUID,
|
||||
ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID,
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (coord.ReferencedSOPSequence === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return sopClassUIDs.includes(
|
||||
coord.ReferencedSOPSequence.ReferencedSOPClassUID
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* New display set doesn't have measurements that references the correct sop class.
|
||||
*/
|
||||
if (measurements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = images.map(i => i.getImageId());
|
||||
const SOPInstanceUIDs = images.map(i => i.SOPInstanceUID);
|
||||
measurements.forEach(measurement => {
|
||||
const { coords } = measurement;
|
||||
|
||||
coords.forEach(coord => {
|
||||
if (coord.ReferencedSOPSequence !== undefined) {
|
||||
const imageIndex = SOPInstanceUIDs.findIndex(
|
||||
SOPInstanceUID =>
|
||||
SOPInstanceUID ===
|
||||
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID
|
||||
);
|
||||
if (imageIndex > -1) {
|
||||
const imageId = imageIds[imageIndex];
|
||||
const imageMetadata = images[imageIndex].getData().metadata;
|
||||
addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
imageMetadata,
|
||||
imageDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default parseSCOORD3D;
|
||||
@@ -0,0 +1,102 @@
|
||||
import csTools from 'cornerstone-tools';
|
||||
import OHIF from '../../../';
|
||||
|
||||
/** Internal imports */
|
||||
import TOOL_NAMES from '../constants/toolNames';
|
||||
import getRenderableData from './getRenderableData';
|
||||
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
csTools.globalImageIdSpecificToolStateManager;
|
||||
|
||||
/**
|
||||
* Add a measurement to a display set.
|
||||
*
|
||||
* @param {*} measurement
|
||||
* @param {*} imageId
|
||||
* @param {*} displaySetInstanceUID
|
||||
*/
|
||||
export default function addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
imageMetadata,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
// TODO -> Render rotated ellipse .
|
||||
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
||||
|
||||
const measurementData = {
|
||||
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
|
||||
renderableData: {},
|
||||
labels: measurement.labels,
|
||||
};
|
||||
|
||||
measurement.coords.forEach(coord => {
|
||||
const { GraphicType, GraphicData, ValueType } = coord;
|
||||
|
||||
if (measurementData.renderableData[GraphicType] === undefined) {
|
||||
measurementData.renderableData[GraphicType] = [];
|
||||
}
|
||||
measurementData.renderableData[GraphicType].push(
|
||||
getRenderableData(GraphicType, GraphicData, ValueType, imageMetadata)
|
||||
);
|
||||
});
|
||||
|
||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
|
||||
if (toolState[imageId] === undefined) {
|
||||
toolState[imageId] = {};
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[imageId];
|
||||
|
||||
// If we don't have tool state for this type of tool, add an empty object
|
||||
if (imageIdToolState[toolName] === undefined) {
|
||||
imageIdToolState[toolName] = {
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
const toolData = imageIdToolState[toolName];
|
||||
|
||||
measurementData.description = `Read-only annotation`;
|
||||
measurementData.isReadOnly = true;
|
||||
toolData.data.push(measurementData);
|
||||
|
||||
addToMeasurementApi({ measurementData, toolName, imageId });
|
||||
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||
|
||||
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
||||
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
||||
// It'd be super werid if it didn't anyway as a SCOORD.
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
|
||||
return measurement;
|
||||
}
|
||||
|
||||
const addToMeasurementApi = ({ measurementData, toolName, imageId }) => {
|
||||
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||
|
||||
const toolType = toolName;
|
||||
const collection = measurementApi.tools[toolType];
|
||||
if (!collection) return;
|
||||
if (!measurementData || measurementData.cancelled) return;
|
||||
|
||||
const imageAttributes = OHIF.measurements.getImageAttributes(null, imageId);
|
||||
const measurement = Object.assign({}, measurementData, imageAttributes, {
|
||||
lesionNamingNumber: measurementData.lesionNamingNumber,
|
||||
userId: OHIF.user.getUserId(),
|
||||
toolType,
|
||||
});
|
||||
|
||||
const addedMeasurement = measurementApi.addMeasurement(toolType, measurement);
|
||||
Object.assign(measurementData, addedMeasurement);
|
||||
|
||||
const measurementLabel = OHIF.measurements.getLabel(measurementData);
|
||||
if (measurementLabel) {
|
||||
measurementData.labels = [measurementLabel];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
const getCoordsFromSCOORDOrSCOORD3D = (graphicItem, displaySet) => {
|
||||
const { ValueType, RelationshipType, GraphicType, GraphicData } = graphicItem;
|
||||
|
||||
// if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) {
|
||||
// console.warn(
|
||||
// `Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."`
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
const coords = { ValueType, GraphicType, GraphicData };
|
||||
|
||||
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
|
||||
if (ValueType === 'SCOORD') {
|
||||
const { ReferencedSOPSequence } = graphicItem.ContentSequence;
|
||||
coords.ReferencedSOPSequence = ReferencedSOPSequence;
|
||||
} else if (ValueType === 'SCOORD3D') {
|
||||
if (graphicItem.ReferencedFrameOfReferenceUID) {
|
||||
coords.ReferencedFrameOfReferenceSequence = graphicItem.ReferencedFrameOfReferenceUID;
|
||||
} else if (graphicItem.ContentSequence) {
|
||||
const {
|
||||
ReferencedFrameOfReferenceSequence,
|
||||
} = graphicItem.ContentSequence;
|
||||
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
|
||||
}
|
||||
}
|
||||
|
||||
return coords;
|
||||
};
|
||||
|
||||
export default getCoordsFromSCOORDOrSCOORD3D;
|
||||
@@ -0,0 +1,19 @@
|
||||
const getLabelFromMeasuredValueSequence = (
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
) => {
|
||||
const { CodeMeaning } = ConceptNameCodeSequence;
|
||||
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
||||
const { CodeValue } = MeasurementUnitsCodeSequence;
|
||||
|
||||
const formatedNumericValue = NumericValue
|
||||
? Number(NumericValue).toFixed(1)
|
||||
: '';
|
||||
|
||||
return {
|
||||
label: CodeMeaning,
|
||||
value: `${formatedNumericValue} ${CodeValue}`,
|
||||
}; // E.g. Long Axis: 31.0 mm
|
||||
};
|
||||
|
||||
export default getLabelFromMeasuredValueSequence;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { CodeNameCodeSequenceValues } from '../enums';
|
||||
import getSequenceAsArray from './getSequenceAsArray';
|
||||
import getMergedContentSequencesByTrackingUniqueIdentifiers from './getMergedContentSequencesByTrackingUniqueIdentifiers';
|
||||
import processMeasurement from './processMeasurement';
|
||||
|
||||
const getMeasurements = (
|
||||
ImagingMeasurementReportContentSequence,
|
||||
displaySet
|
||||
) => {
|
||||
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.ImagingMeasurements
|
||||
);
|
||||
|
||||
const MeasurementGroups = getSequenceAsArray(
|
||||
ImagingMeasurements.ContentSequence
|
||||
).filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.MeasurementGroup
|
||||
);
|
||||
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers = getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
MeasurementGroups
|
||||
);
|
||||
|
||||
let measurements = [];
|
||||
|
||||
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
||||
trackingUniqueIdentifier => {
|
||||
const mergedContentSequence =
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
];
|
||||
|
||||
const measurement = processMeasurement(mergedContentSequence, displaySet);
|
||||
if (measurement) {
|
||||
measurements.push(measurement);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return measurements;
|
||||
};
|
||||
|
||||
export default getMeasurements;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import getSequenceAsArray from './getSequenceAsArray';
|
||||
import { CodeNameCodeSequenceValues } from '../enums';
|
||||
|
||||
const getMergedContentSequencesByTrackingUniqueIdentifiers = MeasurementGroups => {
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
||||
|
||||
MeasurementGroups.forEach(MeasurementGroup => {
|
||||
const ContentSequence = getSequenceAsArray(
|
||||
MeasurementGroup.ContentSequence
|
||||
);
|
||||
|
||||
const TrackingUniqueIdentifierItem = ContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||
);
|
||||
|
||||
if (!TrackingUniqueIdentifierItem) {
|
||||
console.warn(
|
||||
'No Tracking Unique Identifier, skipping ambiguous measurement.'
|
||||
);
|
||||
}
|
||||
|
||||
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
|
||||
|
||||
if (
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
] === undefined
|
||||
) {
|
||||
// Add the full ContentSequence
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
] = [...ContentSequence];
|
||||
} else {
|
||||
// Add the ContentSequence minus the tracking identifier, as we have this
|
||||
// Information in the merged ContentSequence anyway.
|
||||
ContentSequence.forEach(item => {
|
||||
if (
|
||||
item.ConceptNameCodeSequence.CodeValue !==
|
||||
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||
) {
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
].push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mergedContentSequencesByTrackingUniqueIdentifiers;
|
||||
};
|
||||
|
||||
export default getMergedContentSequencesByTrackingUniqueIdentifiers;
|
||||
@@ -0,0 +1,43 @@
|
||||
import getSequenceAsArray from './getSequenceAsArray';
|
||||
import { CodeNameCodeSequenceValues } from '../enums';
|
||||
|
||||
const getReferencedImagesList = ImagingMeasurementReportContentSequence => {
|
||||
const referencedImages = [];
|
||||
|
||||
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.ImageLibrary
|
||||
);
|
||||
|
||||
if (!ImageLibrary.ContentSequence) {
|
||||
return referencedImages;
|
||||
}
|
||||
|
||||
const ImageLibraryGroup = getSequenceAsArray(
|
||||
ImageLibrary.ContentSequence
|
||||
).find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.ImageLibraryGroup
|
||||
);
|
||||
|
||||
getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||
const { ReferencedSOPSequence } = item;
|
||||
if (ReferencedSOPSequence) {
|
||||
const {
|
||||
ReferencedSOPClassUID,
|
||||
ReferencedSOPInstanceUID,
|
||||
} = ReferencedSOPSequence;
|
||||
|
||||
referencedImages.push({
|
||||
ReferencedSOPClassUID,
|
||||
ReferencedSOPInstanceUID,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return referencedImages;
|
||||
};
|
||||
|
||||
export default getReferencedImagesList;
|
||||
@@ -0,0 +1,192 @@
|
||||
import csMath from 'cornerstone-math';
|
||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||
import { inv } from 'mathjs';
|
||||
|
||||
const getRenderableData = (
|
||||
GraphicType,
|
||||
GraphicData,
|
||||
ValueType,
|
||||
imageMetadata
|
||||
) => {
|
||||
let renderableData;
|
||||
|
||||
const orientation = imageMetadata.ImageOrientationPatient;
|
||||
const position = imageMetadata.ImagePositionPatient;
|
||||
const pixelSpacing = imageMetadata.PixelSpacing;
|
||||
const sliceSpacing = imageMetadata.SliceThickness
|
||||
? imageMetadata.SliceThickness
|
||||
: 1;
|
||||
// https://nipy.org/nibabel/dicom/dicom_orientation.html
|
||||
const M = [
|
||||
[
|
||||
orientation[0] * pixelSpacing[0],
|
||||
orientation[3] * pixelSpacing[1],
|
||||
sliceSpacing,
|
||||
position[0],
|
||||
],
|
||||
[
|
||||
orientation[1] * pixelSpacing[0],
|
||||
orientation[4] * pixelSpacing[1],
|
||||
sliceSpacing,
|
||||
position[1],
|
||||
],
|
||||
[
|
||||
orientation[2] * pixelSpacing[0],
|
||||
orientation[5] * pixelSpacing[1],
|
||||
sliceSpacing,
|
||||
position[2],
|
||||
],
|
||||
[0, 0, 0, 1],
|
||||
];
|
||||
|
||||
// we need to go from 3D to pixel (cornerstone2D works in pixel coordinates),
|
||||
// we take the inverse.
|
||||
const M1 = inv(M);
|
||||
|
||||
const worldToIJK = (point, M1) => {
|
||||
const worldPoint = {
|
||||
x:
|
||||
M1[0][0] * point.x + M1[0][1] * point.y + M1[0][2] * point.z + M1[0][3],
|
||||
y:
|
||||
M1[1][0] * point.x + M1[1][1] * point.y + M1[1][2] * point.z + M1[1][3],
|
||||
z:
|
||||
M1[2][0] * point.x + M1[2][1] * point.y + M1[2][2] * point.z + M1[2][3],
|
||||
};
|
||||
return worldPoint;
|
||||
};
|
||||
|
||||
// https://dicom.innolitics.com/ciods/procedure-log/sr-document-content/00700023
|
||||
switch (GraphicType) {
|
||||
case SCOORD_TYPES.POINT:
|
||||
renderableData = [];
|
||||
|
||||
if (ValueType === 'SCOORD3D') {
|
||||
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||
const point = {
|
||||
x: GraphicData[i],
|
||||
y: GraphicData[i + 1],
|
||||
z: GraphicData[i + 2],
|
||||
};
|
||||
|
||||
renderableData.push(worldToIJK(point, M1));
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case SCOORD_TYPES.MULTIPOINT:
|
||||
renderableData = [];
|
||||
|
||||
if (ValueType === 'SCOORD3D') {
|
||||
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||
const point = {
|
||||
x: GraphicData[i],
|
||||
y: GraphicData[i + 1],
|
||||
z: GraphicData[i + 2],
|
||||
};
|
||||
|
||||
renderableData.push(worldToIJK(point, M1));
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case SCOORD_TYPES.POLYLINE:
|
||||
renderableData = [];
|
||||
|
||||
if (ValueType === 'SCOORD3D') {
|
||||
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||
const point = {
|
||||
x: GraphicData[i],
|
||||
y: GraphicData[i + 1],
|
||||
z: GraphicData[i + 2],
|
||||
};
|
||||
|
||||
renderableData.push(worldToIJK(point, M1));
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case SCOORD_TYPES.POLYGON:
|
||||
// this is only scoord3d
|
||||
renderableData = [];
|
||||
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||
const point = {
|
||||
x: GraphicData[i],
|
||||
y: GraphicData[i + 1],
|
||||
z: GraphicData[i + 2],
|
||||
};
|
||||
|
||||
renderableData.push(worldToIJK(point, M1));
|
||||
}
|
||||
break;
|
||||
case SCOORD_TYPES.CIRCLE: {
|
||||
// this is only scoord
|
||||
const center = { x: GraphicData[0], y: GraphicData[1] };
|
||||
const onPerimeter = { x: GraphicData[2], y: GraphicData[3] };
|
||||
|
||||
const radius = csMath.point.distance(center, onPerimeter);
|
||||
|
||||
renderableData = {
|
||||
center,
|
||||
radius,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case SCOORD_TYPES.ELLIPSE: {
|
||||
console.warn('ROTATED ELLIPSE NOT YET SUPPORTED!');
|
||||
// To Do: scoord3d ellips, need data for testing
|
||||
const majorAxis = [
|
||||
{ x: GraphicData[0], y: GraphicData[1] },
|
||||
{ x: GraphicData[2], y: GraphicData[3] },
|
||||
];
|
||||
const minorAxis = [
|
||||
{ x: GraphicData[4], y: GraphicData[5] },
|
||||
{ x: GraphicData[6], y: GraphicData[7] },
|
||||
];
|
||||
|
||||
// Calculate two opposite corners of box defined by two axes.
|
||||
|
||||
const minorAxisLength = csMath.point.distance(minorAxis[0], minorAxis[1]);
|
||||
|
||||
const minorAxisDirection = {
|
||||
x: (minorAxis[1].x - minorAxis[0].x) / minorAxisLength,
|
||||
y: (minorAxis[1].y - minorAxis[0].y) / minorAxisLength,
|
||||
};
|
||||
|
||||
const halfMinorAxisLength = minorAxisLength / 2;
|
||||
|
||||
// First end point of major axis + half minor axis vector
|
||||
const corner1 = {
|
||||
x: majorAxis[0].x + minorAxisDirection.x * halfMinorAxisLength,
|
||||
y: majorAxis[0].y + minorAxisDirection.y * halfMinorAxisLength,
|
||||
};
|
||||
|
||||
// Second end point of major axis - half of minor axis vector
|
||||
const corner2 = {
|
||||
x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength,
|
||||
y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength,
|
||||
};
|
||||
|
||||
renderableData = {
|
||||
corner1,
|
||||
corner2,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return renderableData;
|
||||
};
|
||||
|
||||
export default getRenderableData;
|
||||
@@ -0,0 +1,4 @@
|
||||
const getSequenceAsArray = sequence =>
|
||||
Array.isArray(sequence) ? sequence : [sequence];
|
||||
|
||||
export default getSequenceAsArray;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { adapters } from 'dcmjs';
|
||||
|
||||
const cornerstoneAdapters = adapters.Cornerstone;
|
||||
|
||||
/**
|
||||
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`.
|
||||
*
|
||||
* @param {object} displaySet The SR `displaySet` to check.
|
||||
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `MeasurementService`.
|
||||
* @returns {boolean} True if the SR can be rehydrated into the `MeasurementService`.
|
||||
*/
|
||||
export default function isRehydratable(displaySet, mappings) {
|
||||
if (!mappings || !mappings.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mappingDefinitions = mappings.map(m => m.definition);
|
||||
const { measurements } = displaySet;
|
||||
|
||||
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||
adapterKey =>
|
||||
typeof cornerstoneAdapters[adapterKey]
|
||||
.isValidCornerstoneTrackingIdentifier === 'function'
|
||||
);
|
||||
|
||||
const adapters = [];
|
||||
|
||||
adapterKeys.forEach(key => {
|
||||
if (mappingDefinitions.includes(key)) {
|
||||
// Must have both a dcmjs adapter and a MeasurementService
|
||||
// Definition in order to be a candidate for import.
|
||||
adapters.push(cornerstoneAdapters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < measurements.length; i++) {
|
||||
const TrackingIdentifier = measurements[i].TrackingIdentifier;
|
||||
const hydratable = adapters.some(adapter =>
|
||||
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
|
||||
);
|
||||
|
||||
if (hydratable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import processTID1410Measurement from './processTID1410Measurement';
|
||||
import processNonGeometricallyDefinedMeasurement from './processNonGeometricallyDefinedMeasurement';
|
||||
|
||||
const processMeasurement = (mergedContentSequence, displaySet) => {
|
||||
if (
|
||||
mergedContentSequence.some(
|
||||
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||
)
|
||||
) {
|
||||
return processTID1410Measurement(mergedContentSequence, displaySet);
|
||||
}
|
||||
|
||||
return processNonGeometricallyDefinedMeasurement(mergedContentSequence);
|
||||
};
|
||||
|
||||
export default processMeasurement;
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import getLabelFromMeasuredValueSequence from './getLabelFromMeasuredValueSequence';
|
||||
import getCoordsFromSCOORDOrSCOORD3D from './getCoordsFromSCOORDOrSCOORD3D';
|
||||
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from '../enums';
|
||||
|
||||
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||
|
||||
const processNonGeometricallyDefinedMeasurement = mergedContentSequence => {
|
||||
const NUMContentItems = mergedContentSequence.filter(
|
||||
group => group.ValueType === 'NUM'
|
||||
);
|
||||
|
||||
const UIDREFContentItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'UIDREF'
|
||||
);
|
||||
|
||||
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
);
|
||||
|
||||
const Finding = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.Finding
|
||||
);
|
||||
|
||||
const FindingSites = mergedContentSequence.filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.SRT &&
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.FindingSite
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
loaded: false,
|
||||
labels: [],
|
||||
coords: [],
|
||||
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||
};
|
||||
|
||||
if (
|
||||
Finding &&
|
||||
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||
Finding.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
value: Finding.ConceptCodeSequence.CodeMeaning,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
||||
if (FindingSites.length) {
|
||||
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
||||
FindingSite =>
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
);
|
||||
|
||||
if (cornerstoneFreeTextFindingSite) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
NUMContentItems.forEach(item => {
|
||||
const {
|
||||
ConceptNameCodeSequence,
|
||||
ContentSequence,
|
||||
MeasuredValueSequence,
|
||||
} = item;
|
||||
|
||||
if (!ContentSequence) {
|
||||
console.warn(`Graphic ${ContentSequence} missing, skipping annotation.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { ValueType } = ContentSequence;
|
||||
|
||||
if (!ValueType === 'SCOORD' && !ValueType === 'SCOORD3D') {
|
||||
console.warn(
|
||||
`Graphic ${ValueType} not currently supported, skipping annotation.`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const coords = getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
|
||||
|
||||
if (coords) {
|
||||
measurement.coords.push(coords);
|
||||
}
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return measurement;
|
||||
};
|
||||
|
||||
export default processNonGeometricallyDefinedMeasurement;
|
||||
@@ -0,0 +1,88 @@
|
||||
import getLabelFromMeasuredValueSequence from './getLabelFromMeasuredValueSequence';
|
||||
import getCoordsFromSCOORDOrSCOORD3D from './getCoordsFromSCOORDOrSCOORD3D';
|
||||
import { RELATIONSHIP_TYPE, CodeNameCodeSequenceValues } from '../enums';
|
||||
|
||||
/**
|
||||
* TID 1410 Planar ROI Measurements and Qualitative Evaluations.
|
||||
*
|
||||
* @param {*} mergedContentSequence
|
||||
* @returns
|
||||
*/
|
||||
const processTID1410Measurement = (mergedContentSequence, displaySet) => {
|
||||
// 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
|
||||
|
||||
const graphicItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||
);
|
||||
|
||||
if (!graphicItem) {
|
||||
console.warn(
|
||||
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIDREFContentItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'UIDREF'
|
||||
);
|
||||
|
||||
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
);
|
||||
|
||||
const NUMContentItems = mergedContentSequence.filter(
|
||||
group => group.ValueType === 'NUM'
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
loaded: false,
|
||||
labels: [],
|
||||
coords: [getCoordsFromSCOORDOrSCOORD3D(graphicItem, displaySet)],
|
||||
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||
};
|
||||
|
||||
NUMContentItems.forEach(item => {
|
||||
const {
|
||||
ConceptNameCodeSequence,
|
||||
ContentSequence,
|
||||
MeasuredValueSequence,
|
||||
} = item;
|
||||
|
||||
if (
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.Score
|
||||
) {
|
||||
ContentSequence.forEach(item => {
|
||||
if (
|
||||
[
|
||||
RELATIONSHIP_TYPE.SELECTED_FROM,
|
||||
RELATIONSHIP_TYPE.INFERRED_FROM,
|
||||
].includes(item.RelationshipType)
|
||||
) {
|
||||
if (item.ReferencedSOPSequence) {
|
||||
measurement.coords.forEach(coord => {
|
||||
coord.ReferencedSOPSequence = item.ReferencedSOPSequence;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return measurement;
|
||||
};
|
||||
|
||||
export default processTID1410Measurement;
|
||||
@@ -19,9 +19,10 @@ import findMostRecentStructuredReport from './utils/findMostRecentStructuredRepo
|
||||
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
|
||||
*
|
||||
* @param {serverType} server
|
||||
* @param {object} external
|
||||
* @returns {Promise} Should resolve with OHIF measurementData object
|
||||
*/
|
||||
const retrieveMeasurements = server => {
|
||||
const retrieveMeasurements = (server, external = {}) => {
|
||||
log.info('[DICOMSR] retrieveMeasurements');
|
||||
|
||||
if (!server || server.type !== 'dicomWeb') {
|
||||
@@ -36,7 +37,7 @@ const retrieveMeasurements = server => {
|
||||
|
||||
if (!latestSeries) return Promise.resolve({});
|
||||
|
||||
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
|
||||
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl, external);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,9 +17,15 @@ const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||
* @param {Array} series - List of all series metaData loaded
|
||||
* @param {Array} studies - List of all studies metaData loaded
|
||||
* @param {string} serverUrl - Server URL to be used on request
|
||||
* @param {object} external
|
||||
* @returns {Object} MeasurementData
|
||||
*/
|
||||
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||
const retrieveMeasurementFromSR = async (
|
||||
series,
|
||||
studies,
|
||||
serverUrl,
|
||||
external
|
||||
) => {
|
||||
const config = {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
@@ -40,7 +46,8 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||
const displaySets = getAllDisplaySets(studies);
|
||||
const measurementsData = parseDicomStructuredReport(
|
||||
part10SRArrayBuffer,
|
||||
displaySets
|
||||
displaySets,
|
||||
external
|
||||
);
|
||||
|
||||
return measurementsData;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import classes from '../classes';
|
||||
import parseSCOORD3D from './SCOORD3D/parseSCOORD3D';
|
||||
|
||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||
|
||||
@@ -12,11 +13,19 @@ const { LogManager } = classes;
|
||||
*
|
||||
* @param {ArrayBuffer} part10SRArrayBuffer
|
||||
* @param {Array} displaySets
|
||||
* @param {object} external
|
||||
* @returns
|
||||
*/
|
||||
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
// Get the dicom data as an Object
|
||||
const parseDicomStructuredReport = (
|
||||
part10SRArrayBuffer,
|
||||
displaySets,
|
||||
external
|
||||
) => {
|
||||
if (external && external.servicesManager) {
|
||||
parseSCOORD3D({ servicesManager: external.servicesManager, displaySets });
|
||||
}
|
||||
|
||||
// Get the dicom data as an Object
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
|
||||
@@ -41,6 +41,7 @@ const isStructuredReportSeries = series => {
|
||||
const supportedSopClassUIDs = [
|
||||
'1.2.840.10008.5.1.4.1.1.88.22',
|
||||
'1.2.840.10008.5.1.4.1.1.11.1',
|
||||
'1.2.840.10008.5.1.4.1.1.88.34', // COMPREHENSIVE_3D_SR
|
||||
];
|
||||
|
||||
const firstInstance = series.getFirstInstance();
|
||||
|
||||
@@ -98,7 +98,7 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
constructor(stack, options) {
|
||||
super(stack, options);
|
||||
|
||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandle.bind(
|
||||
this.imageLoadProgressEventHandler = this._imageLoadProgressEventHandler.bind(
|
||||
this
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
import getImageId from '../utils/getImageId.js';
|
||||
|
||||
const noop = () => {};
|
||||
@@ -13,6 +11,7 @@ export class StudyPrefetcher {
|
||||
requestType: 'prefetch',
|
||||
preventCache: false,
|
||||
prefetchDisplaySetsTimeout: 300,
|
||||
maxNumPrefetchRequests: 100,
|
||||
includeActiveDisplaySet: false,
|
||||
};
|
||||
|
||||
@@ -94,7 +93,11 @@ export class StudyPrefetcher {
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
try {
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch {
|
||||
throw new Error('Failed to find the enabled element');
|
||||
}
|
||||
|
||||
this.stopPrefetching();
|
||||
this.prefetchDisplaySets(displaySetInstanceUID);
|
||||
@@ -104,7 +107,7 @@ export class StudyPrefetcher {
|
||||
* Stop prefetching images.
|
||||
*/
|
||||
stopPrefetching() {
|
||||
cornerstoneTools.requestPoolManager.clearRequestStack('prefetch');
|
||||
cornerstone.imageLoadPoolManager.clearRequestStack('prefetch');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +117,11 @@ export class StudyPrefetcher {
|
||||
* @param {number} timeout
|
||||
*/
|
||||
prefetchDisplaySetsAsync(element, timeout) {
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
try {
|
||||
this.enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch {
|
||||
throw new Error('Failed to find the enabled element');
|
||||
}
|
||||
timeout = timeout || this.options.prefetchDisplaySetsTimeout;
|
||||
clearTimeout(this.prefetchDisplaySetsHandler);
|
||||
this.prefetchDisplaySetsHandler = setTimeout(() => {
|
||||
@@ -143,20 +150,29 @@ export class StudyPrefetcher {
|
||||
*/
|
||||
prefetchImageIds(imageIds) {
|
||||
const nonCachedImageIds = this.filterCachedImageIds(imageIds);
|
||||
const requestPoolManager = cornerstoneTools.requestPoolManager;
|
||||
const imageLoadPoolManager = cornerstone.imageLoadPoolManager;
|
||||
|
||||
imageLoadPoolManager.maxNumRequests = {
|
||||
...imageLoadPoolManager.maxNumRequests,
|
||||
prefetch: this.options.maxNumPrefetchRequests,
|
||||
};
|
||||
|
||||
let requestFn;
|
||||
if (this.options.preventCache) {
|
||||
requestFn = id => cornerstone.loadImage(id);
|
||||
} else {
|
||||
requestFn = id => cornerstone.loadAndCacheImage(id);
|
||||
}
|
||||
|
||||
nonCachedImageIds.forEach(imageId => {
|
||||
requestPoolManager.addRequest(
|
||||
{},
|
||||
imageId,
|
||||
imageLoadPoolManager.addRequest(
|
||||
requestFn.bind(this, imageId),
|
||||
this.options.requestType,
|
||||
this.options.preventCache,
|
||||
() => {},
|
||||
noop
|
||||
{
|
||||
imageId,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
requestPoolManager.startGrabbing();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,10 @@ import { SeriesMetadata } from './SeriesMetadata';
|
||||
import { api } from 'dicomweb-client';
|
||||
// - createStacks
|
||||
import { isImage } from '../../utils/isImage';
|
||||
import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable';
|
||||
import {
|
||||
isDisplaySetReconstructable,
|
||||
isSpacingUniform,
|
||||
} from '../../utils/isDisplaySetReconstructable';
|
||||
import errorHandler from '../../errorHandler';
|
||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||
import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook';
|
||||
@@ -159,41 +162,64 @@ class StudyMetadata extends Metadata {
|
||||
// series into another display set.
|
||||
const stackableInstances = [];
|
||||
series.forEachInstance(instance => {
|
||||
let displaySet;
|
||||
|
||||
// All imaging modalities must have a valid value for SOPClassUID (x00080016) or Rows (x00280010)
|
||||
if (
|
||||
!isImage(instance.getTagValue('SOPClassUID')) &&
|
||||
!instance.getTagValue('Rows')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (isMultiFrame(instance)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
|
||||
// we set an empty display and we add a isModalitySupported variable to
|
||||
// print a warning that the modality is not supported in the thumbnail.
|
||||
const displaySet = new ImageSet([]);
|
||||
const seriesData = series.getData();
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
isClip: true,
|
||||
displaySetInstanceUID: displaySet.uid,
|
||||
SeriesDate: seriesData.SeriesDate,
|
||||
SeriesTime: seriesData.SeriesTime,
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
|
||||
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
});
|
||||
displaySets.push(displaySet);
|
||||
} else if (isSingleImageModality(instance.Modality)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
SeriesNumber: instance.getTagValue('SeriesNumber'),
|
||||
SeriesDescription: instance.getTagValue('SeriesDescription'),
|
||||
numImageFrames: instance.getTagValue('NumberOfFrames'),
|
||||
frameRate: instance.getTagValue('FrameTime'),
|
||||
Modality: instance.getTagValue('Modality'),
|
||||
isMultiFrame: false,
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
isReconstructable: false,
|
||||
isModalitySupported: false,
|
||||
metadata: instance.getData().metadata,
|
||||
});
|
||||
|
||||
displaySets.push(displaySet);
|
||||
} else {
|
||||
stackableInstances.push(instance);
|
||||
if (isMultiFrame(instance)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
isClip: true,
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID for drag/drop purposes
|
||||
numImageFrames: instance.getTagValue('NumberOfFrames'), // Override the default value of instances.length
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
});
|
||||
displaySets.push(displaySet);
|
||||
} else if (isSingleImageModality(instance.Modality)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
displaySet.setAttributes({
|
||||
sopClassUIDs,
|
||||
StudyInstanceUID: study.getStudyInstanceUID(), // Include the study instance UID
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
InstanceNumber: instance.getTagValue('InstanceNumber'), // Include the instance number
|
||||
AcquisitionDatetime: instance.getTagValue('AcquisitionDateTime'), // Include the acquisition datetime
|
||||
});
|
||||
displaySets.push(displaySet);
|
||||
} else {
|
||||
stackableInstances.push(instance);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -227,7 +253,6 @@ class StudyMetadata extends Metadata {
|
||||
displaySets.map(displaySet => this._derivedDisplaySets.push(displaySet));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the source display set of the derivated display set.
|
||||
* @param {object} derivatedDisplaySet
|
||||
@@ -242,40 +267,45 @@ class StudyMetadata extends Metadata {
|
||||
});
|
||||
|
||||
const otherDisplaySets = allDisplaySets.filter(
|
||||
ds => ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
|
||||
ds =>
|
||||
ds.displaySetInstanceUID !== derivatedDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const { metadata } = derivatedDisplaySet;
|
||||
|
||||
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence
|
||||
(metadata, otherDisplaySets);
|
||||
let referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromSourceImageSequence(
|
||||
metadata,
|
||||
otherDisplaySets
|
||||
);
|
||||
|
||||
let noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
|
||||
let noReferencedSeriesAvailable =
|
||||
!referencedSeriesInstanceUIDs ||
|
||||
referencedSeriesInstanceUIDs.length === 0;
|
||||
if (noReferencedSeriesAvailable) {
|
||||
referencedSeriesInstanceUIDs =
|
||||
_findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence
|
||||
(metadata);
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence(
|
||||
metadata
|
||||
);
|
||||
}
|
||||
|
||||
noReferencedSeriesAvailable = !referencedSeriesInstanceUIDs ||
|
||||
noReferencedSeriesAvailable =
|
||||
!referencedSeriesInstanceUIDs ||
|
||||
referencedSeriesInstanceUIDs.length === 0;
|
||||
if (noReferencedSeriesAvailable) {
|
||||
referencedSeriesInstanceUIDs =
|
||||
_findReferencedSeriesInstanceUIDsFromReferencedImageSequence
|
||||
(metadata, otherDisplaySets);
|
||||
referencedSeriesInstanceUIDs = _findReferencedSeriesInstanceUIDsFromReferencedImageSequence(
|
||||
metadata,
|
||||
otherDisplaySets
|
||||
);
|
||||
}
|
||||
|
||||
const referencedSeriesAvailable = referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0;
|
||||
const referencedSeriesAvailable =
|
||||
referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0;
|
||||
if (referencedSeriesAvailable) {
|
||||
const referencedDisplaySet = otherDisplaySets.find(ds =>
|
||||
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
|
||||
);
|
||||
;
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of derived datasets in the study, filtered by the given filter.
|
||||
@@ -303,7 +333,10 @@ class StudyMetadata extends Metadata {
|
||||
if (referencedSeriesInstanceUID) {
|
||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
||||
displaySet => {
|
||||
return StudyMetadata.getReferencedDisplaySet(displaySet, [this]).SeriesInstanceUID === referencedSeriesInstanceUID;
|
||||
return (
|
||||
StudyMetadata.getReferencedDisplaySet(displaySet, [this])
|
||||
.SeriesInstanceUID === referencedSeriesInstanceUID
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -311,8 +344,7 @@ class StudyMetadata extends Metadata {
|
||||
if (referencedFrameOfReferenceUID) {
|
||||
filteredDerivedDisplaySets = filteredDerivedDisplaySets.filter(
|
||||
displaySet =>
|
||||
displaySet.FrameOfReferenceUID ===
|
||||
referencedFrameOfReferenceUID
|
||||
displaySet.FrameOfReferenceUID === referencedFrameOfReferenceUID
|
||||
);
|
||||
}
|
||||
|
||||
@@ -497,8 +529,10 @@ class StudyMetadata extends Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._displaySets.some(ds =>
|
||||
ds.displaySetInstanceUID === displaySet.displaySetInstanceUID)
|
||||
if (
|
||||
this._displaySets.some(
|
||||
ds => ds.displaySetInstanceUID === displaySet.displaySetInstanceUID
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -867,8 +901,9 @@ const makeDisplaySet = (series, instances) => {
|
||||
imageSet.sortByImagePositionPatient();
|
||||
|
||||
// check if the spacing is uniform and update isReconstructable
|
||||
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find
|
||||
(issue => issue === ReconstructionIssues.DATASET_4D);
|
||||
const datasetIs4D = displayReconstructableInfo.reconstructionIssues.find(
|
||||
issue => issue === ReconstructionIssues.DATASET_4D
|
||||
);
|
||||
displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D);
|
||||
imageSet.isReconstructable = displaySpacingInfo.isUniform;
|
||||
|
||||
@@ -881,11 +916,15 @@ const makeDisplaySet = (series, instances) => {
|
||||
|
||||
if (!imageSet.displayReconstructableInfo) {
|
||||
// It is not reconstrabale Save type of warning
|
||||
imageSet.reconstructionIssues = displaySpacingInfo ?
|
||||
displayReconstructableInfo.reconstructionIssues.concat(displaySpacingInfo.reconstructionIssues) :
|
||||
displayReconstructableInfo.reconstructionIssues;
|
||||
imageSet.reconstructionIssues = displaySpacingInfo
|
||||
? displayReconstructableInfo.reconstructionIssues.concat(
|
||||
displaySpacingInfo.reconstructionIssues
|
||||
)
|
||||
: displayReconstructableInfo.reconstructionIssues;
|
||||
}
|
||||
|
||||
imageSet.isModalitySupported = true;
|
||||
|
||||
return imageSet;
|
||||
};
|
||||
|
||||
@@ -963,38 +1002,36 @@ function _getDisplaySetFromSopClassModule(
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedSeriesSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence (
|
||||
metadata,
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedSeriesSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedSeriesSequence(
|
||||
metadata
|
||||
) {
|
||||
if (!metadata.ReferencedSeriesSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
let referencedSeriesInstanceUIDs;
|
||||
const ReferencedSeriesSequence = _toArray(
|
||||
metadata.ReferencedSeriesSequence
|
||||
);
|
||||
const ReferencedSeriesSequence = _toArray(metadata.ReferencedSeriesSequence);
|
||||
|
||||
referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
|
||||
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
|
||||
);
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* ReferencedImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence(
|
||||
metadata,
|
||||
displaySets
|
||||
) {
|
||||
@@ -1015,22 +1052,25 @@ function _findReferencedSeriesInstanceUIDsFromReferencedImageSequence (
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
|
||||
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
|
||||
if (
|
||||
referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SourceImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SourceImageSequence.
|
||||
* @param {object} derivatedDisplaySet.metadata
|
||||
* @param {array[object]} displaysets
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSourceImageSequence(
|
||||
metadata,
|
||||
displaySets
|
||||
) {
|
||||
@@ -1040,9 +1080,7 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
SourceImageSequence = metadata.SourceImageSequence;
|
||||
} else {
|
||||
const { PerFrameFunctionalGroupsSequence } = metadata;
|
||||
const firstFunctionalGroups = _toArray(
|
||||
PerFrameFunctionalGroupsSequence
|
||||
)[0];
|
||||
const firstFunctionalGroups = _toArray(PerFrameFunctionalGroupsSequence)[0];
|
||||
if (firstFunctionalGroups) {
|
||||
const { DerivationImageSequence } = firstFunctionalGroups;
|
||||
SourceImageSequence = DerivationImageSequence;
|
||||
@@ -1062,22 +1100,25 @@ function _findReferencedSeriesInstanceUIDsFromSourceImageSequence (
|
||||
displaySets,
|
||||
ReferencedSOPInstanceUID
|
||||
);
|
||||
if (referencedSeriesInstanceUIDs && referencedSeriesInstanceUIDs.length !== 0) {
|
||||
if (
|
||||
referencedSeriesInstanceUIDs &&
|
||||
referencedSeriesInstanceUIDs.length !== 0
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return referencedSeriesInstanceUIDs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SOPInstanceUID of the displaySets.
|
||||
* @param {array[object]} displaysets
|
||||
* @param {string} SOPInstanceUID
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
|
||||
* Returns the referenced series instance UIDs by searching the information in the
|
||||
* SOPInstanceUID of the displaySets.
|
||||
* @param {array[object]} displaysets
|
||||
* @param {string} SOPInstanceUID
|
||||
* @return {array[string]} referenced series instance UIDs.
|
||||
*/
|
||||
function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID(
|
||||
displaySets,
|
||||
SOPInstanceUID
|
||||
) {
|
||||
@@ -1098,10 +1139,10 @@ function _findReferencedSeriesInstanceUIDsFromSOPInstanceUID (
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _toArray(arrayOrObject) {
|
||||
return Array.isArray(arrayOrObject) ? arrayOrObject : [arrayOrObject];
|
||||
}
|
||||
|
||||
export {StudyMetadata};
|
||||
export { StudyMetadata };
|
||||
@@ -20,6 +20,7 @@ import ui from './ui';
|
||||
import user from './user.js';
|
||||
import errorHandler from './errorHandler.js';
|
||||
import utils, { hotkeys } from './utils/';
|
||||
import str2ab from './utils/str2ab';
|
||||
|
||||
import {
|
||||
UINotificationService,
|
||||
@@ -96,6 +97,8 @@ export {
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
LoggerService,
|
||||
|
||||
str2ab,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('Top level exports', () => {
|
||||
'cornerstone',
|
||||
'default', //
|
||||
'errorHandler',
|
||||
'str2ab',
|
||||
'string',
|
||||
'ui',
|
||||
'user',
|
||||
|
||||
@@ -802,6 +802,10 @@ export default class MeasurementApi {
|
||||
collection.push(addedMeasurement);
|
||||
}
|
||||
|
||||
if (measurement.isReadOnly) {
|
||||
addedMeasurement.isReadOnly = measurement.isReadOnly;
|
||||
}
|
||||
|
||||
if (!emptyItem) {
|
||||
// Reflect the entry in the tool group collection
|
||||
groupCollection.push({
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
export default function(element) {
|
||||
// Get the Cornerstone imageId
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const imageId = enabledElement.image.imageId;
|
||||
export default function(element, imageId) {
|
||||
if (!imageId) {
|
||||
// Get the Cornerstone imageId
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
imageId = enabledElement.image.imageId;
|
||||
}
|
||||
|
||||
// Get StudyInstanceUID & PatientID
|
||||
const {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export const dicomSRDisplayTool = {
|
||||
id: 'DICOMSRDisplayTool',
|
||||
name: 'DICOMSRDisplayTool',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'DICOMSRDisplayTool',
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction: data => {
|
||||
return `(SR) ${data.lesionNamingNumber ||
|
||||
data.measurementNumber ||
|
||||
data.text ||
|
||||
''}`;
|
||||
},
|
||||
},
|
||||
caseProgress: {
|
||||
include: true,
|
||||
evaluate: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default dicomSRDisplayTool;
|
||||
@@ -10,6 +10,7 @@ import { angle } from './angle';
|
||||
import { targetCR } from './targetCR';
|
||||
import { targetNE } from './targetNE';
|
||||
import { targetUN } from './targetUN';
|
||||
import dicomSRDisplayTool from './dicomSRDisplayTool';
|
||||
|
||||
export {
|
||||
arrowAnnotate,
|
||||
@@ -24,4 +25,5 @@ export {
|
||||
targetCR,
|
||||
targetNE,
|
||||
targetUN,
|
||||
dicomSRDisplayTool,
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Internal imports */
|
||||
import log from '../../log';
|
||||
import guid from '../../utils/guid';
|
||||
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||
|
||||
/**
|
||||
* Measurement source schema
|
||||
@@ -15,7 +17,7 @@ import guid from '../../utils/guid';
|
||||
*
|
||||
* @typedef {Object} Measurement
|
||||
* @property {number} id -
|
||||
* @property {string} sopInstanceUid -
|
||||
* @property {string} SOPInstanceUID -
|
||||
* @property {string} FrameOfReferenceUID -
|
||||
* @property {string} referenceSeriesUID -
|
||||
* @property {string} label -
|
||||
@@ -32,24 +34,37 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'id',
|
||||
'SOPInstanceUID',
|
||||
'FrameOfReferenceUID',
|
||||
'referenceStudyUID',
|
||||
'referenceSeriesUID',
|
||||
'label',
|
||||
'description',
|
||||
'type',
|
||||
'unit',
|
||||
'area', // TODO: Add concept names instead (descriptor)
|
||||
'mean',
|
||||
'stdDev',
|
||||
'length',
|
||||
'shortestDiameter',
|
||||
'longestDiameter',
|
||||
'text', // NOTE: There is nothing like this in SR.
|
||||
'points',
|
||||
'source',
|
||||
];
|
||||
|
||||
const EVENTS = {
|
||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
|
||||
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
|
||||
};
|
||||
|
||||
const VALUE_TYPES = {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
@@ -61,6 +76,7 @@ class MeasurementService {
|
||||
this.mappings = {};
|
||||
this.measurements = {};
|
||||
this.listeners = {};
|
||||
this._jumpToMeasurementCache = {};
|
||||
Object.defineProperty(this, 'EVENTS', {
|
||||
value: EVENTS,
|
||||
writable: false,
|
||||
@@ -73,6 +89,8 @@ class MeasurementService {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,7 +109,7 @@ class MeasurementService {
|
||||
/**
|
||||
* Get specific measurement by its id.
|
||||
*
|
||||
* @param {string} id If of the measurement
|
||||
* @param {string} id Id of the measurement
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
getMeasurement(id) {
|
||||
@@ -114,13 +132,11 @@ class MeasurementService {
|
||||
*/
|
||||
createSource(name, version) {
|
||||
if (!name) {
|
||||
log.warn('Source name not provided. Exiting early.');
|
||||
return;
|
||||
throw new Error('Source name not provided.');
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
log.warn('Source version not provided. Exiting early.');
|
||||
return;
|
||||
throw new Error('Source version not provided.');
|
||||
}
|
||||
|
||||
const id = guid();
|
||||
@@ -132,6 +148,9 @@ class MeasurementService {
|
||||
source.addOrUpdate = (definition, measurement) => {
|
||||
return this.addOrUpdate(source, definition, measurement);
|
||||
};
|
||||
source.remove = id => {
|
||||
return this.remove(id, source);
|
||||
};
|
||||
source.getAnnotation = (definition, measurementId) => {
|
||||
return this.getAnnotation(source, definition, measurementId);
|
||||
};
|
||||
@@ -142,6 +161,32 @@ class MeasurementService {
|
||||
return source;
|
||||
}
|
||||
|
||||
getSource(name, version) {
|
||||
const { sources } = this;
|
||||
const id = this._getSourceId(name, version);
|
||||
|
||||
return sources[id];
|
||||
}
|
||||
|
||||
getSourceMappings(name, version) {
|
||||
const { mappings } = this;
|
||||
const id = this._getSourceId(name, version);
|
||||
|
||||
return mappings[id];
|
||||
}
|
||||
|
||||
_getSourceId(name, version) {
|
||||
const { sources } = this;
|
||||
|
||||
const sourceId = Object.keys(sources).find(sourceId => {
|
||||
const source = sources[sourceId];
|
||||
|
||||
return source.name === name && source.version === version;
|
||||
});
|
||||
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new measurement matching criteria along with mapping functions.
|
||||
*
|
||||
@@ -160,28 +205,23 @@ class MeasurementService {
|
||||
toMeasurementSchema
|
||||
) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
throw new Error('Invalid source.');
|
||||
}
|
||||
|
||||
if (!matchingCriteria) {
|
||||
log.warn('Matching criteria not provided. Exiting early.');
|
||||
return;
|
||||
throw new Error('Matching criteria not provided.');
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
log.warn('Definition not provided. Exiting early.');
|
||||
return;
|
||||
throw new Error('Definition not provided.');
|
||||
}
|
||||
|
||||
if (!toSourceSchema) {
|
||||
log.warn('Source mapping function not provided. Exiting early.');
|
||||
return;
|
||||
throw new Error('Mapping function to source schema not provided.');
|
||||
}
|
||||
|
||||
if (!toMeasurementSchema) {
|
||||
log.warn('Measurement mapping function not provided. Exiting early.');
|
||||
return;
|
||||
throw new Error('Measurement mapping function not provided.');
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
@@ -227,9 +267,9 @@ class MeasurementService {
|
||||
measurementId,
|
||||
definition
|
||||
);
|
||||
const measurement = this.getMeasurement(measurementId);
|
||||
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
||||
|
||||
const measurement = this.getMeasurement(measurementId);
|
||||
const matchingMapping = this._getMatchingMapping(
|
||||
source,
|
||||
definition,
|
||||
@@ -243,15 +283,50 @@ class MeasurementService {
|
||||
}
|
||||
}
|
||||
|
||||
update(id, measurement, notYetUpdatedAtSource = false) {
|
||||
if (this.measurements[id]) {
|
||||
const updatedMeasurement = {
|
||||
...measurement,
|
||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
log.info(
|
||||
`Updating internal measurement representation...`,
|
||||
updatedMeasurement
|
||||
);
|
||||
|
||||
this.measurements[id] = updatedMeasurement;
|
||||
|
||||
this.publish(
|
||||
// Add an internal flag to say the measurement has not yet been updated at source.
|
||||
this.EVENTS.MEASUREMENT_UPDATED,
|
||||
{
|
||||
source: measurement.source,
|
||||
measurement: updatedMeasurement,
|
||||
notYetUpdatedAtSource,
|
||||
}
|
||||
);
|
||||
|
||||
return updatedMeasurement.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or update persisted measurements.
|
||||
*
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {Measurement} measurement The source measurement
|
||||
* @return {string} A measurement id
|
||||
* Add a raw measurement into a source so that it may be
|
||||
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||
* Of the same form as the measurement source.
|
||||
* @param {MeasurementSource} source The measurement source instance.
|
||||
* @param {string} definition The source definition you want to add the measurement to.
|
||||
* @param {object} data The data you wish to add to the source.
|
||||
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
|
||||
*/
|
||||
addOrUpdate(source, definition, sourceMeasurement) {
|
||||
addRawMeasurement(
|
||||
source,
|
||||
definition,
|
||||
data,
|
||||
toMeasurementSchema,
|
||||
dataSource = {}
|
||||
) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
@@ -273,14 +348,8 @@ class MeasurementService {
|
||||
|
||||
let measurement = {};
|
||||
try {
|
||||
const sourceMappings = this.mappings[source.id];
|
||||
const { toMeasurementSchema } = sourceMappings.find(
|
||||
mapping => mapping.definition === definition
|
||||
);
|
||||
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(sourceMeasurement);
|
||||
|
||||
measurement = toMeasurementSchema(data);
|
||||
/* Assign measurement source instance */
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
@@ -298,7 +367,7 @@ class MeasurementService {
|
||||
return;
|
||||
}
|
||||
|
||||
let internalId = sourceMeasurement.id;
|
||||
let internalId = data.id;
|
||||
if (!internalId) {
|
||||
internalId = guid();
|
||||
log.warn(`Measurement ID not found. Generating UID: ${internalId}`);
|
||||
@@ -316,49 +385,167 @@ class MeasurementService {
|
||||
newMeasurement
|
||||
);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastChange(
|
||||
this.EVENTS.MEASUREMENT_UPDATED,
|
||||
this.publish(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source,
|
||||
newMeasurement
|
||||
);
|
||||
measurement: newMeasurement,
|
||||
});
|
||||
} else {
|
||||
log.info(`Measurement added.`, newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastChange(
|
||||
this.EVENTS.MEASUREMENT_ADDED,
|
||||
this.publish(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||
source,
|
||||
newMeasurement
|
||||
);
|
||||
measurement: newMeasurement,
|
||||
data,
|
||||
dataSource,
|
||||
});
|
||||
}
|
||||
|
||||
return newMeasurement.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to measurement updates.
|
||||
* Adds or update persisted measurements.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {Function} callback Events callback
|
||||
* @return {Object} Observable object with actions
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {Measurement} measurement The source measurement
|
||||
* @return {string} A measurement id
|
||||
*/
|
||||
subscribe(eventName, callback) {
|
||||
if (this._isValidEvent(eventName)) {
|
||||
const listenerId = guid();
|
||||
const subscription = { id: listenerId, callback };
|
||||
|
||||
console.info(`Subscribing to '${eventName}'.`);
|
||||
if (Array.isArray(this.listeners[eventName])) {
|
||||
this.listeners[eventName].push(subscription);
|
||||
} else {
|
||||
this.listeners[eventName] = [subscription];
|
||||
}
|
||||
|
||||
return {
|
||||
unsubscribe: () => this._unsubscribe(eventName, listenerId),
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Event ${eventName} not supported.`);
|
||||
addOrUpdate(source, definition, sourceMeasurement) {
|
||||
if (!this._isValidSource(source)) {
|
||||
throw new Error('Invalid source.');
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
throw new Error('No source definition provided.');
|
||||
}
|
||||
|
||||
const sourceInfo = this._getSourceInfo(source);
|
||||
|
||||
if (!this._sourceHasMappings(source)) {
|
||||
throw new Error(
|
||||
`No measurement mappings found for '${sourceInfo}' source. Exiting early.`
|
||||
);
|
||||
}
|
||||
|
||||
let measurement = {};
|
||||
try {
|
||||
const sourceMappings = this.mappings[source.id];
|
||||
const { toMeasurementSchema } = sourceMappings.find(
|
||||
mapping => mapping.definition === definition
|
||||
);
|
||||
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(sourceMeasurement);
|
||||
|
||||
/* Assign measurement source instance */
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to map '${sourceInfo}' measurement for definition ${definition}:`,
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
if (!this._isValidMeasurement(measurement)) {
|
||||
throw new Error(
|
||||
`Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.`
|
||||
);
|
||||
}
|
||||
|
||||
let internalId = sourceMeasurement.id;
|
||||
if (!internalId) {
|
||||
internalId = guid();
|
||||
log.info(`Measurement ID not found. Generating UID: ${internalId}`);
|
||||
}
|
||||
|
||||
const newMeasurement = {
|
||||
...measurement,
|
||||
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||
id: internalId,
|
||||
};
|
||||
|
||||
if (this.measurements[internalId]) {
|
||||
log.info(
|
||||
`Measurement already defined. Updating measurement.`,
|
||||
newMeasurement
|
||||
);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this.publish(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
notYetUpdatedAtSource: false,
|
||||
});
|
||||
} else {
|
||||
log.info('Measurement added.', newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this.publish(this.EVENTS.MEASUREMENT_ADDED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
});
|
||||
}
|
||||
|
||||
return newMeasurement.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a measurement and broadcasts the removed event.
|
||||
*
|
||||
* @param {string} id The measurement id
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
* @return {string} The removed measurement id
|
||||
*/
|
||||
remove(id, source) {
|
||||
if (!id || !this.measurements[id]) {
|
||||
log.warn(`No id provided, or unable to find measurement by id.`);
|
||||
return;
|
||||
}
|
||||
|
||||
delete this.measurements[id];
|
||||
this.publish(this.EVENTS.MEASUREMENT_REMOVED, {
|
||||
source,
|
||||
measurement: id, // This is weird :shrug:
|
||||
});
|
||||
}
|
||||
|
||||
clearMeasurements() {
|
||||
this.measurements = {};
|
||||
this._jumpToMeasurementCache = {};
|
||||
this.publish(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||
}
|
||||
|
||||
jumpToMeasurement(viewportIndex, id) {
|
||||
const measurement = this.measurements[id];
|
||||
|
||||
if (!measurement) {
|
||||
log.warn(`No id provided, or unable to find measurement by id.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._addJumpToMeasurement(viewportIndex, id);
|
||||
|
||||
const eventName = this.EVENTS.JUMP_TO_MEASUREMENT;
|
||||
|
||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||
|
||||
if (hasListeners && hasCallbacks) {
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener.callback({ viewportIndex, measurement });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_addJumpToMeasurement(viewportIndex, id) {
|
||||
this._jumpToMeasurementCache[viewportIndex] = id;
|
||||
}
|
||||
|
||||
getJumpToMeasurement(viewportIndex) {
|
||||
return this._jumpToMeasurementCache[viewportIndex];
|
||||
}
|
||||
|
||||
removeJumpToMeasurement(viewportIndex) {
|
||||
delete this._jumpToMeasurementCache[viewportIndex];
|
||||
}
|
||||
|
||||
_getMappingByMeasurementSource(measurementId, definition) {
|
||||
@@ -370,12 +557,20 @@ class MeasurementService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all measurements and broadcasts cleared event.
|
||||
*/
|
||||
clear() {
|
||||
this.measurements = {};
|
||||
this.publish(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get measurement mapping function if matching criteria.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {string} measurement The measurement serice measurement
|
||||
* @param {Measurement} measurement The measurement service measurement
|
||||
* @return {Object} The mapping based on matched criteria
|
||||
*/
|
||||
_getMatchingMapping(source, definition, measurement) {
|
||||
@@ -387,10 +582,27 @@ class MeasurementService {
|
||||
|
||||
/* Criteria Matching */
|
||||
return sourceMappingsByDefinition.find(({ matchingCriteria }) => {
|
||||
return (
|
||||
if (matchingCriteria.type !== measurement.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
matchingCriteria.properties &&
|
||||
matchingCriteria.properties.every(name =>
|
||||
measurement.hasOwnProperty(name)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
measurement.points &&
|
||||
measurement.points.length === matchingCriteria.points
|
||||
);
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -426,47 +638,6 @@ class MeasurementService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts measurement changes.
|
||||
*
|
||||
* @param {string} measurementId The measurement id
|
||||
* @param {MeasurementSource} source The measurement source
|
||||
* @param {string} eventName The event name
|
||||
* @return void
|
||||
*/
|
||||
_broadcastChange(eventName, source, measurement) {
|
||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||
|
||||
if (hasListeners && hasCallbacks) {
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener.callback({ source, measurement });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe to measurement updates.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {string} listenerId The listeners id
|
||||
* @return void
|
||||
*/
|
||||
_unsubscribe(eventName, listenerId) {
|
||||
if (!this.listeners[eventName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = this.listeners[eventName];
|
||||
if (Array.isArray(listeners)) {
|
||||
this.listeners[eventName] = listeners.filter(
|
||||
({ id }) => id !== listenerId
|
||||
);
|
||||
} else {
|
||||
this.listeners[eventName] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given measurement data is valid.
|
||||
*
|
||||
@@ -489,10 +660,10 @@ class MeasurementService {
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @return {boolean} Event name validation
|
||||
*/
|
||||
_isValidEvent(eventName) {
|
||||
return Object.values(this.EVENTS).includes(eventName);
|
||||
}
|
||||
// */
|
||||
// _isValidEvent(eventName) {
|
||||
// return Object.values(this.EVENTS).includes(eventName);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Converts object of objects to array.
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('MeasurementService.js', () => {
|
||||
let source;
|
||||
let definition;
|
||||
let matchingCriteria;
|
||||
let toAnnotation;
|
||||
let toSourceSchema;
|
||||
let toMeasurement;
|
||||
let annotation;
|
||||
|
||||
@@ -34,11 +34,20 @@ describe('MeasurementService.js', () => {
|
||||
unit: 'mm',
|
||||
area: 123,
|
||||
type: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: [{ x: 1, y: 2 }, { x: 1, y: 2 }],
|
||||
points: [
|
||||
{ x: 1, y: 2 },
|
||||
{ x: 1, y: 2 },
|
||||
],
|
||||
source: source,
|
||||
};
|
||||
toAnnotation = () => annotation;
|
||||
toMeasurement = () => measurement;
|
||||
toSourceSchema = () => annotation;
|
||||
toMeasurement = () => {
|
||||
if (Object.keys(measurement).includes('invalidProperty')) {
|
||||
throw new Error('Measurement does not match schema');
|
||||
}
|
||||
|
||||
return measurement;
|
||||
}
|
||||
matchingCriteria = {
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
@@ -52,16 +61,16 @@ describe('MeasurementService.js', () => {
|
||||
measurementService.createSource('Testing', '1');
|
||||
});
|
||||
|
||||
it('logs warning and return early if no name provided', () => {
|
||||
measurementService.createSource(null, '1');
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
it('throws Error if no name provided', () => {
|
||||
expect(() => {
|
||||
measurementService.createSource(null, '1')
|
||||
}).toThrow(new Error('Source name not provided.'));
|
||||
});
|
||||
|
||||
it('logs warning and return early if no version provided', () => {
|
||||
measurementService.createSource('Testing', null);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
it('throws Error if no version provided', () => {
|
||||
expect(() => {
|
||||
measurementService.createSource('Testing', null)
|
||||
}).toThrow(new Error('Source version not provided.'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,83 +80,84 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no matching criteria provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
null,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
it('throws Error if invalid source provided', () => {
|
||||
expect(() => {
|
||||
const invalidSource = {};
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
measurementService.addMapping(
|
||||
invalidSource,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
}).toThrow(new Error('Invalid source.'));
|
||||
});
|
||||
|
||||
it('logs warning and return early if invalid source provided', () => {
|
||||
const invalidSoure = {};
|
||||
|
||||
measurementService.addMapping(
|
||||
invalidSoure,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
it('throws Error if no matching criteria provided', () => {
|
||||
expect(() => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
null,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
}).toThrow(new Error('Matching criteria not provided.'));
|
||||
});
|
||||
|
||||
it('logs warning and return early if no source provided', () => {
|
||||
measurementService.addMapping(
|
||||
null /* source */,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
it('throws Error if no source provided', () => {
|
||||
expect(() => {
|
||||
measurementService.addMapping(
|
||||
null /* source */,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
}).toThrow(new Error('Invalid source.'));
|
||||
});
|
||||
|
||||
it('logs warning and return early if no definition provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
null /* definition */,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
expect(() => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
null /* definition */,
|
||||
matchingCriteria,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
}).toThrow(new Error('Definition not provided.'));
|
||||
});
|
||||
|
||||
it('logs warning and return early if no measurement mapping function provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
null /* toAnnotation */,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
it('throws Error if no measurement mapping function provided', () => {
|
||||
expect(() => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
null /* toSourceSchema */,
|
||||
toMeasurement
|
||||
);
|
||||
}).toThrow(new Error('Mapping function to source schema not provided.'));
|
||||
});
|
||||
|
||||
it('logs warning and return early if no annotation mapping function provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
null /* toMeasurement */
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
it('throws Error if no annotation mapping function provided', () => {
|
||||
expect(() => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toSourceSchema,
|
||||
null /* toMeasurement */
|
||||
);
|
||||
}).toThrow(new Error('Measurement mapping function not provided.'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -157,7 +167,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
const measurementId = source.addOrUpdate(definition, annotation);
|
||||
@@ -171,7 +181,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
{},
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
const measurementId = source.addOrUpdate(definition, annotation);
|
||||
@@ -193,7 +203,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -212,7 +222,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -232,7 +242,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -245,9 +255,9 @@ describe('MeasurementService.js', () => {
|
||||
});
|
||||
|
||||
it('fails to add new measurements when no mapping', () => {
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
expect(() => {
|
||||
source.addOrUpdate(definition, measurement);
|
||||
}).toThrow()
|
||||
});
|
||||
|
||||
it('fails to add new measurements when invalid mapping function', () => {
|
||||
@@ -255,13 +265,13 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
1 /* Invalid */
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
expect(() => {
|
||||
source.addOrUpdate(definition, measurement);
|
||||
}).toThrow()
|
||||
});
|
||||
|
||||
it('adds new measurement with custom id', () => {
|
||||
@@ -271,7 +281,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -288,28 +298,28 @@ describe('MeasurementService.js', () => {
|
||||
expect(newMeasurement).toEqual(savedMeasurement);
|
||||
});
|
||||
|
||||
it('logs warning and return if adding invalid measurement', () => {
|
||||
it('throws Error if adding invalid measurement', () => {
|
||||
measurement.invalidProperty = {};
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
expect(() => {
|
||||
source.addOrUpdate(definition, measurement);
|
||||
}).toThrow()
|
||||
});
|
||||
|
||||
it('updates existent measurement', () => {
|
||||
it('updates existing measurement', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -330,7 +340,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -354,7 +364,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
@@ -381,7 +391,7 @@ describe('MeasurementService.js', () => {
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toSourceSchema,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import pubSubServiceInterface from './pubSubServiceInterface';
|
||||
|
||||
export { pubSubServiceInterface };
|
||||
@@ -0,0 +1,88 @@
|
||||
import guid from '../../utils/guid';
|
||||
|
||||
/**
|
||||
* Consumer must implement:
|
||||
* this.listeners = {}
|
||||
* this.EVENTS = { "EVENT_KEY": "EVENT_VALUE" }
|
||||
*/
|
||||
export default {
|
||||
subscribe,
|
||||
publish,
|
||||
_unsubscribe,
|
||||
_isValidEvent,
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to updates.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {Function} callback Events callback
|
||||
* @return {Object} Observable object with actions
|
||||
*/
|
||||
function subscribe(eventName, callback) {
|
||||
if (this._isValidEvent(eventName)) {
|
||||
const listenerId = guid();
|
||||
const subscription = { id: listenerId, callback };
|
||||
|
||||
console.info(`Subscribing to '${eventName}'.`);
|
||||
if (Array.isArray(this.listeners[eventName])) {
|
||||
this.listeners[eventName].push(subscription);
|
||||
} else {
|
||||
this.listeners[eventName] = [subscription];
|
||||
}
|
||||
|
||||
return {
|
||||
unsubscribe: () => this._unsubscribe(eventName, listenerId),
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Event ${eventName} not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe to measurement updates.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {string} listenerId The listeners id
|
||||
* @return void
|
||||
*/
|
||||
function _unsubscribe(eventName, listenerId) {
|
||||
if (!this.listeners[eventName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listeners = this.listeners[eventName];
|
||||
if (Array.isArray(listeners)) {
|
||||
this.listeners[eventName] = listeners.filter(({ id }) => id !== listenerId);
|
||||
} else {
|
||||
this.listeners[eventName] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given event is valid.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @return {boolean} Event name validation
|
||||
*/
|
||||
function _isValidEvent(eventName) {
|
||||
return Object.values(this.EVENTS).includes(eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts changes.
|
||||
*
|
||||
* @param {string} eventName - The event name
|
||||
* @param {func} callbackProps - Properties to pass callback
|
||||
* @return void
|
||||
*/
|
||||
function publish(eventName, callbackProps) {
|
||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||
|
||||
if (hasListeners && hasCallbacks) {
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener.callback(callbackProps);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
/**
|
||||
* An implementation of the static wado client, that fetches data from
|
||||
* a static response rather than actually doing real queries. This allows
|
||||
* fast encoding of test data, but because it is static, anything actually
|
||||
* performing searches doesn't work. This version fixes the query issue
|
||||
* by manually implementing a query option.
|
||||
*/
|
||||
export default class StaticWadoClient extends api.DICOMwebClient {
|
||||
static filterKeys = {
|
||||
"StudyInstanceUID": "0020000D",
|
||||
"PatientName": "00100010",
|
||||
"00100020": "mrn",
|
||||
"PatientID": "00100020",
|
||||
"StudyDescription": "00081030",
|
||||
"StudyDate": "00080020",
|
||||
"ModalitiesInStudy": "00080061",
|
||||
AccessionNumber: "00080050",
|
||||
};
|
||||
|
||||
constructor(qidoConfig) {
|
||||
super(qidoConfig);
|
||||
this.staticWado = qidoConfig.staticWado;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the search for studies remote query with a local version which
|
||||
* retrieves a complete query list and then sub-selects from it locally.
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async searchForStudies(options) {
|
||||
if (!this.staticWado) return super.searchForStudies(options);
|
||||
|
||||
let searchResult = await super.searchForStudies(options);
|
||||
const { queryParams } = options;
|
||||
if (!queryParams) return searchResult;
|
||||
const filtered = searchResult.filter(study => {
|
||||
for (const key of Object.keys(StaticWadoClient.filterKeys)) {
|
||||
if (!this.filterItem(key, queryParams, study)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares values, matching any instance of desired to any instance of
|
||||
* actual by recursively go through the paired set of values. That is,
|
||||
* this is O(m*n) where m is how many items in desired and n is the length of actual
|
||||
* Then, at the individual item node, compares the Alphabetic name if present,
|
||||
* and does a sub-string matching on string values, and otherwise does an
|
||||
* exact match comparison.
|
||||
*
|
||||
* @param {*} desired
|
||||
* @param {*} actual
|
||||
* @returns true if the values match
|
||||
*/
|
||||
compareValues(desired, actual) {
|
||||
if (Array.isArray(desired)) {
|
||||
return desired.find(item => this.compareValues(item, actual));
|
||||
}
|
||||
if (Array.isArray(actual)) {
|
||||
return actual.find(actualItem => this.compareValues(desired, actualItem));
|
||||
}
|
||||
if (actual && actual.Alphabetic) {
|
||||
actual = actual.Alphabetic;
|
||||
}
|
||||
if (typeof (actual) == 'string') {
|
||||
if (actual.length === 0) return true;
|
||||
if (desired.length === 0 || desired === '*') return true;
|
||||
if (desired[0] === '*' && desired[desired.length - 1] === '*') {
|
||||
console.log(`Comparing ${actual} to ${desired.substring(1, desired.length - 1)}`)
|
||||
return actual.indexOf(desired.substring(1, desired.length - 1)) != -1;
|
||||
} else if (desired[desired.length - 1] === '*') {
|
||||
return actual.indexOf(desired.substring(0, desired.length - 1)) != -1;
|
||||
} else if (desired[0] === '*') {
|
||||
return actual.indexOf(desired.substring(1)) === actual.length - desired.length + 1;
|
||||
}
|
||||
}
|
||||
return desired === actual;
|
||||
}
|
||||
|
||||
/** Compares a pair of dates to see if the value is within the range */
|
||||
compareDateRange(range, value) {
|
||||
if (!value) return true;
|
||||
const dash = range.indexOf('-');
|
||||
if (dash === -1) return this.compareValues(range, value);
|
||||
const start = range.substring(0, dash);
|
||||
const end = range.substring(dash + 1);
|
||||
return (!start || value >= start) &&
|
||||
(!end || value <= end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the return list by the query parameters.
|
||||
*
|
||||
* @param {*} key
|
||||
* @param {*} queryParams
|
||||
* @param {*} study
|
||||
* @returns
|
||||
*/
|
||||
filterItem(key, queryParams, study) {
|
||||
const altKey = StaticWadoClient.filterKeys[key] || key;
|
||||
if (!queryParams) return true;
|
||||
const testValue = queryParams[key] || queryParams[altKey];
|
||||
if (!testValue) return true;
|
||||
const valueElem = study[key] || study[altKey];
|
||||
if (!valueElem) return false;
|
||||
if (valueElem.vr == 'DA') return this.compareDateRange(testValue, valueElem.Value[0]);
|
||||
const value = valueElem.Value;
|
||||
return this.compareValues(testValue, value) && true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import StaticWadoClient from './StaticWadoClient';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
|
||||
import errorHandler from '../../../errorHandler';
|
||||
@@ -115,14 +116,18 @@ function resultDataToStudies(resultData) {
|
||||
}
|
||||
|
||||
export default function Studies(server, filter) {
|
||||
const { staticWado } = server;
|
||||
const config = {
|
||||
...server,
|
||||
url: server.qidoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
requestHooks: [getXHRRetryRequestHook()],
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const dicomWeb = staticWado
|
||||
? new StaticWadoClient(config)
|
||||
: new api.DICOMwebClient(config);
|
||||
server.qidoSupportsIncludeField =
|
||||
server.qidoSupportsIncludeField === undefined
|
||||
? true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import StaticWadoClient from '../qido/StaticWadoClient';
|
||||
import dcmjs from 'dcmjs';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
@@ -73,7 +73,8 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
configLoad() {
|
||||
const { server } = this;
|
||||
|
||||
const client = new api.DICOMwebClient({
|
||||
const client = new StaticWadoClient({
|
||||
...server,
|
||||
url: server.qidoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
errorInterceptor: errorHandler.getHTTPErrorHandler(),
|
||||
|
||||
@@ -116,6 +116,11 @@ async function makeSOPInstance(server, study, instance) {
|
||||
};
|
||||
study.seriesMap[SeriesInstanceUID] = series;
|
||||
study.series.push(series);
|
||||
} else {
|
||||
if (series.SeriesDate === undefined)
|
||||
series.SeriesDate = naturalizedInstance.SeriesDate;
|
||||
if (series.SeriesTime === undefined)
|
||||
series.SeriesTime = naturalizedInstance.SeriesTime;
|
||||
}
|
||||
|
||||
const wadouri = buildInstanceWadoUrl(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ReconstructionIssues } from './../enums.js';
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
@@ -78,7 +77,9 @@ function processSingleframe(instances) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS);
|
||||
} else if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS);
|
||||
} else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) {
|
||||
} else if (
|
||||
!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)
|
||||
) {
|
||||
reconstructionIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION);
|
||||
}
|
||||
|
||||
@@ -92,7 +93,10 @@ function processSingleframe(instances) {
|
||||
reconstructionIssues.push(ReconstructionIssues.DATASET_4D);
|
||||
}
|
||||
|
||||
return { value: reconstructionIssues.length === 0 ? true : false, reconstructionIssues };
|
||||
return {
|
||||
value: reconstructionIssues.length === 0 ? true : false,
|
||||
reconstructionIssues,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -116,14 +120,12 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
// If spacing is on a uniform grid but we are missing frames,
|
||||
// Allow reconstruction, but pass back the number of missing frames.
|
||||
if (n > 2) {
|
||||
const lastIpp = instances[n - 1].getData().metadata
|
||||
.ImagePositionPatient;
|
||||
const lastIpp = instances[n - 1].getData().metadata.ImagePositionPatient;
|
||||
|
||||
// We can't reconstruct if we are missing ImagePositionPatient values
|
||||
if (firstImagePositionPatient && lastIpp) {
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(n - 1);
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) / (n - 1);
|
||||
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
|
||||
@@ -136,7 +138,7 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
previousImagePositionPatient
|
||||
);
|
||||
|
||||
if (datasetIs4D && spacingBetweenFrames < 1.e-3) {
|
||||
if (datasetIs4D && spacingBetweenFrames < 1e-3) {
|
||||
// the dataset is 4D, if the distance is zero, means that we are
|
||||
// checking the 4th dimension. Do not return, since we want still to
|
||||
// check the 3rd dimension spacing.
|
||||
@@ -164,10 +166,13 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
}
|
||||
}
|
||||
|
||||
return { isUniform: reconstructionIssues.length === 0 ? true : false, missingFrames, reconstructionIssues };
|
||||
return {
|
||||
isUniform: reconstructionIssues.length === 0 ? true : false,
|
||||
missingFrames,
|
||||
reconstructionIssues,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if 4D dataset.
|
||||
*
|
||||
@@ -181,7 +186,7 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
*
|
||||
* @returns {boolean} dataset4D value.
|
||||
*/
|
||||
function _isDataset4D(instances) {
|
||||
function _isDataset4D(instances) {
|
||||
const n = instances.length;
|
||||
for (let ii = 0; ii < n; ++ii) {
|
||||
const instanceMetadataControl = instances[ii].getData().metadata;
|
||||
@@ -204,7 +209,12 @@ function isSpacingUniform(instances, datasetIs4D) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_isSameArray(instanceMetadataControl.ImagePositionPatient, instanceMetadata.ImagePositionPatient)) {
|
||||
if (
|
||||
_isSameArray(
|
||||
instanceMetadataControl.ImagePositionPatient,
|
||||
instanceMetadata.ImagePositionPatient
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -265,11 +275,11 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
||||
function _getPerpendicularDistance(a, b) {
|
||||
return Math.sqrt(
|
||||
Math.pow(a[0] - b[0], 2) +
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
);
|
||||
}
|
||||
|
||||
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
||||
|
||||
export {isDisplaySetReconstructable, isSpacingUniform};
|
||||
export { isDisplaySetReconstructable, isSpacingUniform };
|
||||
@@ -3,6 +3,49 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.5...@ohif/ui@1.10.6) (2022-02-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.4...@ohif/ui@1.10.5) (2021-11-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.3...@ohif/ui@1.10.4) (2021-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.2...@ohif/ui@1.10.3) (2021-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.1...@ohif/ui@1.10.2) (2021-09-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* throttle thumbnail progress updates ([#2557](https://github.com/OHIF/Viewers/issues/2557)) ([66c587b](https://github.com/OHIF/Viewers/commit/66c587b139835201331a64bc24a17901e7583649))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.0...@ohif/ui@1.10.1) (2021-09-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "1.10.1",
|
||||
"version": "1.10.6",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -139,7 +139,6 @@ class MeasurementTable extends Component {
|
||||
const itemIndex = measurement.itemNumber || index + 1;
|
||||
const itemClass =
|
||||
selectedKey === key && !this.props.readOnly ? 'selected' : '';
|
||||
|
||||
return (
|
||||
<MeasurementTableItem
|
||||
key={key}
|
||||
|
||||
@@ -22,11 +22,15 @@ class MeasurementTableItem extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { warningTitle = '', hasWarnings } = this.props.measurementData;
|
||||
const {
|
||||
warningTitle = '',
|
||||
hasWarnings,
|
||||
isReadOnly,
|
||||
} = this.props.measurementData;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{hasWarnings ? (
|
||||
{hasWarnings && !isReadOnly ? (
|
||||
<OverlayTrigger
|
||||
key={this.props.itemIndex}
|
||||
placement="left"
|
||||
@@ -44,8 +48,8 @@ class MeasurementTableItem extends Component {
|
||||
<div>{this.getTableListItem()}</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||
)}
|
||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -62,9 +66,11 @@ class MeasurementTableItem extends Component {
|
||||
};
|
||||
|
||||
getTableListItem = () => {
|
||||
const hasWarningClass = this.props.measurementData.hasWarnings
|
||||
? 'hasWarnings'
|
||||
: '';
|
||||
const hasWarningClass =
|
||||
this.props.measurementData.hasWarnings &&
|
||||
!this.props.measurementData.isReadOnly
|
||||
? 'hasWarnings'
|
||||
: '';
|
||||
|
||||
const actionButtons = [];
|
||||
|
||||
@@ -102,7 +108,9 @@ class MeasurementTableItem extends Component {
|
||||
})}
|
||||
</div>
|
||||
<div className="displayTexts">{this.getDataDisplayText()}</div>
|
||||
<div className="rowActions">{actionButtons}</div>
|
||||
{!this.props.measurementData.isReadOnly && (
|
||||
<div className="rowActions">{actionButtons}</div>
|
||||
)}
|
||||
</div>
|
||||
</TableListItem>
|
||||
);
|
||||
|
||||
@@ -59,9 +59,9 @@ function ImageThumbnail(props) {
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.isCanceled) return;
|
||||
setLoading(false);
|
||||
setError(true);
|
||||
throw new Error(error);
|
||||
// setLoading(false);
|
||||
// setError(true);
|
||||
// throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@ function StudyBrowser(props) {
|
||||
altImageText,
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
InstanceNumber,
|
||||
derivedDisplaySetsNumber,
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
hasWarnings,
|
||||
hasDerivedDisplaySets,
|
||||
} = thumb;
|
||||
|
||||
return (
|
||||
@@ -48,12 +49,13 @@ function StudyBrowser(props) {
|
||||
// Thumb
|
||||
altImageText={altImageText}
|
||||
imageId={imageId}
|
||||
InstanceNumber={InstanceNumber}
|
||||
derivedDisplaySetsNumber={derivedDisplaySetsNumber}
|
||||
displaySetInstanceUID={displaySetInstanceUID} // used by drop
|
||||
numImageFrames={numImageFrames}
|
||||
SeriesDescription={SeriesDescription}
|
||||
SeriesNumber={SeriesNumber}
|
||||
hasWarnings={hasWarnings}
|
||||
hasDerivedDisplaySets={hasDerivedDisplaySets}
|
||||
// Events
|
||||
onClick={onThumbnailClick.bind(
|
||||
undefined,
|
||||
@@ -83,7 +85,7 @@ StudyBrowser.propTypes = {
|
||||
altImageText: PropTypes.string,
|
||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||
imageId: PropTypes.string,
|
||||
InstanceNumber: PropTypes.number,
|
||||
derivedDisplaySetsNumber: PropTypes.number,
|
||||
numImageFrames: PropTypes.number,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.number,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { useDrag } from 'react-dnd';
|
||||
import { classes } from '@ohif/core';
|
||||
import ImageThumbnail from './ImageThumbnail';
|
||||
@@ -14,11 +15,12 @@ const StudyLoadingListener = classes.StudyLoadingListener;
|
||||
function ThumbnailFooter({
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
hasWarnings,
|
||||
hasDerivedDisplaySets,
|
||||
}) {
|
||||
const [inconsistencyWarnings, inconsistencyWarningsSet] = useState([]);
|
||||
const [derivedDisplaySetsActive, derivedDisplaySetsActiveSet] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
@@ -27,10 +29,15 @@ function ThumbnailFooter({
|
||||
inconsistencyWarningsSet(response);
|
||||
}
|
||||
});
|
||||
hasDerivedDisplaySets.then(response => {
|
||||
if (!unmounted) {
|
||||
derivedDisplaySetsActiveSet(response);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unmounted = true;
|
||||
};
|
||||
}, [hasWarnings]);
|
||||
}, [hasWarnings, hasDerivedDisplaySets]);
|
||||
|
||||
const infoOnly = !SeriesDescription;
|
||||
|
||||
@@ -87,20 +94,47 @@ function ThumbnailFooter({
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getDerivedInfo = derivedDisplaySetsActive => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{derivedDisplaySetsActive ? (
|
||||
<div className="derived">
|
||||
<Icon name="link" />
|
||||
</div>
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const getSeriesInformation = (
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
inconsistencyWarnings
|
||||
inconsistencyWarnings,
|
||||
derivedDisplaySetsActive
|
||||
) => {
|
||||
if (!SeriesNumber && !InstanceNumber && !numImageFrames) {
|
||||
if (!SeriesNumber && !numImageFrames) {
|
||||
return;
|
||||
}
|
||||
const seriesInformation = (
|
||||
<div className="series-information">
|
||||
{getInfo(SeriesNumber, 'S:')}
|
||||
{getInfo(InstanceNumber, 'I:')}
|
||||
{getInfo(numImageFrames, '', 'image-frames')}
|
||||
<React.Fragment>
|
||||
{SeriesNumber !== undefined ? (
|
||||
getInfo(SeriesNumber, 'S:')
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
<React.Fragment>
|
||||
{numImageFrames !== undefined ? (
|
||||
getInfo(numImageFrames, '', 'image-frames')
|
||||
) : (
|
||||
<React.Fragment></React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
{getDerivedInfo(derivedDisplaySetsActive)}
|
||||
{getWarningInfo(SeriesNumber, inconsistencyWarnings)}
|
||||
</div>
|
||||
);
|
||||
@@ -113,9 +147,9 @@ function ThumbnailFooter({
|
||||
<div className="series-description">{SeriesDescription}</div>
|
||||
{getSeriesInformation(
|
||||
SeriesNumber,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
inconsistencyWarnings
|
||||
inconsistencyWarnings,
|
||||
derivedDisplaySetsActive
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -129,11 +163,6 @@ function Thumbnail(props) {
|
||||
displaySetInstanceUID,
|
||||
imageId,
|
||||
imageSrc,
|
||||
InstanceNumber,
|
||||
numImageFrames,
|
||||
SeriesDescription,
|
||||
SeriesNumber,
|
||||
hasWarnings,
|
||||
StudyInstanceUID,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
@@ -144,13 +173,13 @@ function Thumbnail(props) {
|
||||
|
||||
const [stackPercentComplete, setStackPercentComplete] = useState(0);
|
||||
useEffect(() => {
|
||||
const onProgressChange = ({ detail }) => {
|
||||
const onProgressChange = throttle(({ detail }) => {
|
||||
const { progressId, progressData } = detail;
|
||||
if (`StackProgress:${displaySetInstanceUID}` === progressId) {
|
||||
const percent = progressData ? progressData.percentComplete : 0;
|
||||
setStackPercentComplete(percent);
|
||||
}
|
||||
};
|
||||
}, 100);
|
||||
|
||||
document.addEventListener(
|
||||
StudyLoadingListener.events.OnProgress,
|
||||
@@ -231,8 +260,8 @@ Thumbnail.propTypes = {
|
||||
altImageText: PropTypes.string,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
InstanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
hasWarnings: PropTypes.instanceOf(Promise),
|
||||
hasDerivedDisplaySets: PropTypes.instanceOf(Promise),
|
||||
numImageFrames: PropTypes.number,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
|
||||
@@ -41,7 +41,7 @@ const SnackbarProvider = ({ children, service }) => {
|
||||
LogManager.subscribe(LogManager.EVENTS.OnLog, onLogHandler);
|
||||
|
||||
return () => {
|
||||
LogManager.subscribe(LogManager.EVENTS.OnLog, onLogHandler);
|
||||
LogManager.unsubscribe(LogManager.EVENTS.OnLog, onLogHandler);
|
||||
};
|
||||
}, [show]);
|
||||
|
||||
|
||||
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user