Compare commits
98
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d57158b59d | ||
|
|
4a7ce1c093 | ||
|
|
333677c40e | ||
|
|
b459b894bd | ||
|
|
c5a4860209 | ||
|
|
4f79bda7c9 | ||
|
|
7d22bb7d5a | ||
|
|
36822cc6ce | ||
|
|
e676f0b5b5 | ||
|
|
57de66a65f | ||
|
|
99022f2bac | ||
|
|
47b1e2837f | ||
|
|
d604ebaffd | ||
|
|
c1a0d3c662 | ||
|
|
16866a47a0 | ||
|
|
77ab0ad9a1 | ||
|
|
1c306dd243 | ||
|
|
404d52fe5c | ||
|
|
d9a50b6b08 | ||
|
|
e41d339f5f | ||
|
|
e374fe2118 | ||
|
|
9feadd3c6d | ||
|
|
3b5f71b0be | ||
|
|
b269415048 | ||
|
|
c9e421cff3 | ||
|
|
d9999c1ae4 | ||
|
|
31ba042121 | ||
|
|
2747effd9e | ||
|
|
6ed1e47a3d | ||
|
|
e8842cf8ae | ||
|
|
d426b78967 | ||
|
|
1d85f7a74e | ||
|
|
8833731ad3 | ||
|
|
bb7cce4716 | ||
|
|
4ad3ab93b0 | ||
|
|
3e074da64e | ||
|
|
d66bad664a | ||
|
|
fa47db4a54 | ||
|
|
e1c21a7ce1 | ||
|
|
c785a5e975 | ||
|
|
f133be75d1 | ||
|
|
b880663818 | ||
|
|
e160889605 | ||
|
|
2f0d1b651b | ||
|
|
d034006294 | ||
|
|
2f459a0f64 | ||
|
|
ee80e02661 | ||
|
|
4e53fbfe9d | ||
|
|
64dfbeab7a | ||
|
|
9c1112f768 | ||
|
|
9b3be9b0c0 | ||
|
|
6829542c00 | ||
|
|
846c9c1c54 | ||
|
|
85fed49eb1 | ||
|
|
298da03e2f | ||
|
|
141e1cf648 | ||
|
|
ea2ab1a25e | ||
|
|
3d0357c9c0 | ||
|
|
f68aaf973c | ||
|
|
f04d459a63 | ||
|
|
4cf9f2c9cb | ||
|
|
c0cc9752c6 | ||
|
|
2910d38668 | ||
|
|
6c950a9669 | ||
|
|
06075ecb51 | ||
|
|
509a7b7e57 | ||
|
|
4bbf24b250 | ||
|
|
ebc01a8ca2 | ||
|
|
4fd0c6dfcb | ||
|
|
06a4af06fa | ||
|
|
6cba41dd8b | ||
|
|
2f30e7a821 | ||
|
|
9a62c28b3f | ||
|
|
98a649b455 | ||
|
|
3529474a16 | ||
|
|
0c37a406d9 | ||
|
|
e8d56c11f0 | ||
|
|
a6b92a4505 | ||
|
|
a4c9a86772 | ||
|
|
73e440968c | ||
|
|
3f7eb8a5d3 | ||
|
|
7b2d44f2c1 | ||
|
|
225da81135 | ||
|
|
686d12da5c | ||
|
|
c22756051b | ||
|
|
8fdfcc0c7d | ||
|
|
44ab65c981 | ||
|
|
b9574b6efc | ||
|
|
96a8e26786 | ||
|
|
80f5b23622 | ||
|
|
2b4950e954 | ||
|
|
e16f14089d | ||
|
|
c3807a89ed | ||
|
|
049ef4a666 | ||
|
|
d5fa880a29 | ||
|
|
1b9a6db73f | ||
|
|
ed2692908e | ||
|
|
0c6c5a5d51 |
No files matched your search
+5
-1
@@ -12,11 +12,15 @@ exemptLabels:
|
||||
- 'Task: CI/Tooling :robot:'
|
||||
- 'Task: Docs 📖'
|
||||
- 'Task: Docs :book:'
|
||||
- 'Task: Refactor 🛠'
|
||||
- 'Task: Refactor :hammer_and_wrench:'
|
||||
- 'Task: Tests :microscope:'
|
||||
- 'PR: Awaiting Review 👀'
|
||||
- 'Triage :white_flag:'
|
||||
- 'Extension: Discussion'
|
||||
- 'Announcement 🎉'
|
||||
- 'IDC:priority'
|
||||
- 'IDC:candidate'
|
||||
- 'IDC:collaboration'
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: 'Stale :baguette_bread:'
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
Vendored
+4
-1
@@ -28,5 +28,8 @@
|
||||
],
|
||||
"prettier.disableLanguages": ["html"],
|
||||
"prettier.endOfLine": "lf",
|
||||
"workbench.colorCustomizations": {}
|
||||
"workbench.colorCustomizations": {},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
from: 'right',
|
||||
// The target component to toggle open/close
|
||||
target: 'target-component-id',
|
||||
// UI Hint; If the target panel is in a "disabled" state
|
||||
isDisabled: studies => {
|
||||
return false;
|
||||
},
|
||||
// Overrides `defaultContext`, if specified
|
||||
context: ['ACTIVE_VIEWPORT:MAGIC'],
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ An extension can register a SOP Class Handler Module by defining a
|
||||
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
|
||||
own components. It instead defines:
|
||||
|
||||
- `sopClassUids`: an array of string SOP Class UIDs that the
|
||||
- `sopClassUIDs`: an array of string SOP Class UIDs that the
|
||||
`getDisplaySetFromSeries` method should be applied to.
|
||||
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
||||
display set
|
||||
@@ -15,13 +15,13 @@ A `displaySet` has the following shape:
|
||||
```js
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: 0,
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
```
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||
sopClassUIDs: Object.values(SOP_CLASS_UIDS),
|
||||
|
||||
/**
|
||||
* @param {object} series -
|
||||
@@ -63,12 +63,12 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0,
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Lesion Tracker - Audit Trail
|
||||
|
||||
1. To view Audit Trails, select **View Audit Log** on the Configuration menu.
|
||||
2. To filter audit logs for a column or multiple columns, type or select in the related column or columns.
|
||||
2. To filter audit logs for a column or multiple columns, type or select in the
|
||||
related column or columns.
|
||||
|
||||

|
||||
@@ -2,50 +2,74 @@
|
||||
|
||||

|
||||
|
||||
1. Study List allows to sort each column. To sort studies, click the desired column header.
|
||||
2. Study List is filterable for each column and you can filter by more than one column by typing in the column field. To filter a column, type the text in the desired column header field and press Enter.
|
||||
3. Pagination is provided for the Study List at the bottom of the page. You can change the number of rows per page or the current page.
|
||||
1. Study List allows to sort each column. To sort studies, click the desired
|
||||
column header.
|
||||
2. Study List is filterable for each column and you can filter by more than one
|
||||
column by typing in the column field. To filter a column, type the text in
|
||||
the desired column header field and press Enter.
|
||||
3. Pagination is provided for the Study List at the bottom of the page. You can
|
||||
change the number of rows per page or the current page.
|
||||
|
||||
## Study List Context Menu
|
||||
|
||||
When you right-click on the study row, Study Context Menu will pop up. Study Context Menu includes operations at the study level.
|
||||
When you right-click on the study row, Study Context Menu will pop up. Study
|
||||
Context Menu includes operations at the study level.
|
||||
|
||||
There are a couple of ways to view a study:
|
||||
|
||||
* Simply double-click on the study row.
|
||||
* Select **View** option on the Study Context Menu, then you will be redirected to Viewer page.
|
||||
- Simply double-click on the study row.
|
||||
- Select **View** option on the Study Context Menu, then you will be redirected
|
||||
to Viewer page.
|
||||
|
||||

|
||||
|
||||
### Timepoint Association
|
||||
|
||||
1. To link a study with a timepoint, right-click on the study row and select **Associate** option on the Study Context Menu. You will see Study Association dialog.
|
||||
2. To link multiple studies at the same time, hold Shift key and select studies you would like to associate, then right-click and select Associate on the Study Context Menu
|
||||
1. To link a study with a timepoint, right-click on the study row and select
|
||||
**Associate** option on the Study Context Menu. You will see Study
|
||||
Association dialog.
|
||||
2. To link multiple studies at the same time, hold Shift key and select studies
|
||||
you would like to associate, then right-click and select Associate on the
|
||||
Study Context Menu
|
||||
|
||||

|
||||
|
||||
The Study Association dialog allows you to select and unselect Timepoint Type for the selected study or studies. Also, it will include all studies within 14 days of your selected studies, in case you forgot to select a study. After you identify timepoints for the selected study or studies, click **Save** to save the association.
|
||||
The Study Association dialog allows you to select and unselect Timepoint Type
|
||||
for the selected study or studies. Also, it will include all studies within 14
|
||||
days of your selected studies, in case you forgot to select a study. After you
|
||||
identify timepoints for the selected study or studies, click **Save** to save
|
||||
the association.
|
||||
|
||||

|
||||
|
||||
### Remove Timepoint Association
|
||||
To unlink a timepoint association with a study, right-click on the study row and select **Remove Association** option on the Study Context Menu. Remove Association will be disabled if the study is already dissociated.
|
||||
|
||||
To unlink a timepoint association with a study, right-click on the study row and
|
||||
select **Remove Association** option on the Study Context Menu. Remove
|
||||
Association will be disabled if the study is already dissociated.
|
||||
|
||||

|
||||
|
||||
### View Series Details
|
||||
To view details of related series, right-click on the study row and select **View Series Details** option on the Study Context Menu, then Series Details dialog will pop up. Series Details dialog gives a summary of the selected study.
|
||||
|
||||
To view details of related series, right-click on the study row and select
|
||||
**View Series Details** option on the Study Context Menu, then Series Details
|
||||
dialog will pop up. Series Details dialog gives a summary of the selected study.
|
||||
|
||||

|
||||
|
||||
### Anonymize (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Send (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Export (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Delete (Under Development)
|
||||
|
||||
...
|
||||
@@ -21,7 +21,6 @@ export default {
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
*/
|
||||
|
||||
getViewportModule() {
|
||||
return '... react component ...';
|
||||
},
|
||||
@@ -64,7 +63,7 @@ const commandsModule = {
|
||||
*/
|
||||
const sopClassHandlerModule = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values({
|
||||
sopClassUIDs: Object.values({
|
||||
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',
|
||||
@@ -78,12 +77,12 @@ const sopClassHandlerModule = {
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0, //utils.guid(),
|
||||
displaySetInstanceUID: 0, //utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
@@ -98,6 +97,9 @@ const panelModule = {
|
||||
icon: 'th-list',
|
||||
label: 'Segments',
|
||||
target: 'segment-panel',
|
||||
isDisabled: studies => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
|
||||
@@ -3,6 +3,80 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.3...@ohif/extension-cornerstone@2.5.0) (2020-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.2...@ohif/extension-cornerstone@2.4.3) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.1...@ohif/extension-cornerstone@2.4.2) (2020-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove Eraser and ROI Window ([6c950a9](https://github.com/OHIF/Viewers/commit/6c950a9669f7fbf3c46e48679fa26ee514824156))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.0...@ohif/extension-cornerstone@2.4.1) (2020-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump react-cornerstone-viewport version to address critical issue ([#1473](https://github.com/OHIF/Viewers/issues/1473)) ([ee80e02](https://github.com/OHIF/Viewers/commit/ee80e026610442e94caf5e4e3e4d193220cd0ece))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.1...@ohif/extension-cornerstone@2.4.0) (2020-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.0...@ohif/extension-cornerstone@2.3.1) (2020-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("../../babel.config.js");
|
||||
@@ -0,0 +1,13 @@
|
||||
const base = require('../../jest.config.base.js');
|
||||
const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
name: pkg.name,
|
||||
displayName: pkg.name,
|
||||
// rootDir: "../.."
|
||||
// testMatch: [
|
||||
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
|
||||
// "<rootDir>/platform/viewer/**/*.test.js"
|
||||
// ]
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.2.2",
|
||||
"version": "2.5.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -25,16 +25,18 @@
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.12.0",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dcmjs": "^0.10.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -49,7 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react-cornerstone-viewport": "2.x.x"
|
||||
"react-cornerstone-viewport": "^2.3.6"
|
||||
}
|
||||
}
|
||||
@@ -4,28 +4,9 @@ import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
const SOP_CLASSES = {
|
||||
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
};
|
||||
|
||||
const specialCaseHandlers = {};
|
||||
specialCaseHandlers[
|
||||
SOP_CLASSES.SEGMENTATION_STORAGE
|
||||
] = handleSegmentationStorage;
|
||||
|
||||
class OHIFCornerstoneViewport extends Component {
|
||||
state = {
|
||||
viewportData: null,
|
||||
@@ -58,34 +39,34 @@ class OHIFCornerstoneViewport extends Component {
|
||||
* Obtain the CornerstoneTools Stack for the specified display set.
|
||||
*
|
||||
* @param {Object[]} studies
|
||||
* @param {String} studyInstanceUid
|
||||
* @param {String} displaySetInstanceUid
|
||||
* @param {String} [sopInstanceUid]
|
||||
* @param {String} StudyInstanceUID
|
||||
* @param {String} displaySetInstanceUID
|
||||
* @param {String} [SOPInstanceUID]
|
||||
* @param {Number} [frameIndex=1]
|
||||
* @return {Object} CornerstoneTools Stack
|
||||
*/
|
||||
static getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex = 0
|
||||
) {
|
||||
if (!studies || !studies.length) {
|
||||
throw new Error('Studies not provided.');
|
||||
}
|
||||
|
||||
if (!studyInstanceUid) {
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
if (!displaySetInstanceUid) {
|
||||
if (!displaySetInstanceUID) {
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
@@ -93,7 +74,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
}
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
@@ -107,17 +88,14 @@ class OHIFCornerstoneViewport extends Component {
|
||||
const stack = Object.assign({}, storedStack);
|
||||
stack.currentImageIdIndex = frameIndex;
|
||||
|
||||
if (sopInstanceUid) {
|
||||
if (SOPInstanceUID) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
'sopCommonModule',
|
||||
const imageIdSOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === sopInstanceUid;
|
||||
return imageIdSOPInstanceUID === SOPInstanceUID;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
@@ -134,44 +112,26 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
getViewportData = async (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
let viewportData;
|
||||
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
const specialCaseHandler =
|
||||
specialCaseHandlers[SOP_CLASSES.SEGMENTATION_STORAGE];
|
||||
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
viewportData = await specialCaseHandler(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
break;
|
||||
default:
|
||||
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
viewportData = {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack,
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
viewportData = {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
stack,
|
||||
};
|
||||
|
||||
return viewportData;
|
||||
};
|
||||
@@ -179,31 +139,28 @@ class OHIFCornerstoneViewport extends Component {
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUIDs,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
if (sopClassUIDs && sopClassUIDs.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids && sopClassUids[0];
|
||||
|
||||
this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
).then(viewportData => {
|
||||
this.setState({
|
||||
@@ -217,13 +174,13 @@ class OHIFCornerstoneViewport extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUid !==
|
||||
prevDisplaySet.displaySetInstanceUid ||
|
||||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
|
||||
@@ -74,10 +74,6 @@ const commandsModule = ({ servicesManager }) => {
|
||||
}
|
||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||
},
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (!element) {
|
||||
@@ -245,7 +241,20 @@ const commandsModule = ({ servicesManager }) => {
|
||||
},
|
||||
setCornerstoneLayout: () => {
|
||||
setCornerstoneLayout();
|
||||
}
|
||||
},
|
||||
setWindowLevel: ({ viewports, window, level }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
|
||||
viewport.voi = {
|
||||
windowWidth: Number(window),
|
||||
windowCenter: Number(level),
|
||||
};
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@@ -334,30 +343,28 @@ const commandsModule = ({ servicesManager }) => {
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
nextViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setZoomTool: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: { toolName: 'Zoom' },
|
||||
},
|
||||
setCornerstoneLayout: {
|
||||
commandFn: actions.setCornerstoneLayout,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { StackManager, DicomLoaderService } = OHIF.utils;
|
||||
|
||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
return study.displaySets.filter(set => {
|
||||
return set.seriesInstanceUid === seriesInstanceUid;
|
||||
});
|
||||
}
|
||||
|
||||
function parseSeg(arrayBuffer, imageIds) {
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
cornerstone.metaData
|
||||
);
|
||||
}
|
||||
|
||||
function addSegMetadataToCornerstoneToolState(
|
||||
segMetadata,
|
||||
toolState,
|
||||
displaySetInstanceUid
|
||||
) {
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
||||
toolState
|
||||
);
|
||||
|
||||
const brushModule = cornerstoneTools.store.modules.brush;
|
||||
|
||||
for (let i = 0; i < segMetadata.length; i++) {
|
||||
brushModule.setters.metadata(displaySetInstanceUid, i, segMetadata[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
displaySet,
|
||||
studies
|
||||
);
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const displaySets = getDisplaySetsBySeries(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid
|
||||
);
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same seriesInstanceUid. This is not supported yet...'
|
||||
);
|
||||
}
|
||||
|
||||
const referenceDisplaySet = displaySets[0];
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.getImageId());
|
||||
const results = parseSeg(arrayBuffer, imageIds);
|
||||
|
||||
if (!results) {
|
||||
throw new Error('Fractional segmentations are not supported');
|
||||
}
|
||||
|
||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
0, // TODO -> Can define a color LUT based on colors in the SEG later.
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame
|
||||
);
|
||||
|
||||
const cachedStack = StackManager.findOrCreateStack(
|
||||
study,
|
||||
referenceDisplaySet
|
||||
);
|
||||
const stack = Object.assign({}, cachedStack);
|
||||
|
||||
stack.currentImageIdIndex = 0;
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack,
|
||||
};
|
||||
}
|
||||
|
||||
export default handleSegmentationStorage;
|
||||
@@ -3,27 +3,8 @@ import { SimpleDialog } from '@ohif/ui';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import merge from 'lodash.merge';
|
||||
import queryString from 'query-string';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If you call for an WADO-URI imageId and get no
|
||||
// metadata, try reformatting to WADO-RS imageId
|
||||
const qs = queryString.parse(imageId);
|
||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||
qs.seriesUID
|
||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
|
||||
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||
}
|
||||
|
||||
// Add this fallback provider with a low priority so it is handled last
|
||||
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -32,9 +13,9 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
if (UIDialogService) {
|
||||
let dialogId = UIDialogService.create({
|
||||
centralize: true,
|
||||
@@ -57,16 +38,13 @@ export default function init({ servicesManager, configuration }) {
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||
|
||||
// ~~ Set our MetadataProvider
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
metadataProvider.get.bind(metadataProvider),
|
||||
9999
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
// ~~
|
||||
const defaultCsToolsConfig = csToolsConfig || {
|
||||
globalToolSyncEnabled: true,
|
||||
@@ -80,7 +58,6 @@ export default function init({ servicesManager, configuration }) {
|
||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
||||
annotations: [
|
||||
csTools.ArrowAnnotateTool,
|
||||
csTools.EraserTool,
|
||||
csTools.BidirectionalTool,
|
||||
csTools.LengthTool,
|
||||
csTools.AngleTool,
|
||||
@@ -89,7 +66,6 @@ export default function init({ servicesManager, configuration }) {
|
||||
csTools.DragProbeTool,
|
||||
csTools.RectangleRoiTool,
|
||||
],
|
||||
segmentation: [csTools.BrushTool],
|
||||
other: [
|
||||
csTools.PanTool,
|
||||
csTools.ZoomTool,
|
||||
@@ -107,6 +83,9 @@ export default function init({ servicesManager, configuration }) {
|
||||
tools.push(...toolsGroupedByType[toolsGroup])
|
||||
);
|
||||
|
||||
/* Measurement Service */
|
||||
_connectToolsToMeasurementService(MeasurementService);
|
||||
|
||||
/* Add extension tools configuration here. */
|
||||
const internalToolsConfig = {
|
||||
ArrowAnnotate: {
|
||||
@@ -182,3 +161,116 @@ export default function init({ servicesManager, configuration }) {
|
||||
csTools.setToolActive('ZoomTouchPinch', {});
|
||||
csTools.setToolEnabled('Overlay', {});
|
||||
}
|
||||
|
||||
const _initMeasurementService = measurementService => {
|
||||
/* Initialization */
|
||||
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(
|
||||
measurementService
|
||||
);
|
||||
const csToolsVer4MeasurementSource = measurementService.createSource(
|
||||
'CornerstoneTools',
|
||||
'4'
|
||||
);
|
||||
|
||||
/* Matching Criterias */
|
||||
const matchingCriteria = {
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
};
|
||||
|
||||
/* Mappings */
|
||||
measurementService.addMapping(
|
||||
csToolsVer4MeasurementSource,
|
||||
'Length',
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
return csToolsVer4MeasurementSource;
|
||||
};
|
||||
|
||||
const _connectToolsToMeasurementService = measurementService => {
|
||||
const csToolsVer4MeasurementSource = _initMeasurementService(
|
||||
measurementService
|
||||
);
|
||||
const {
|
||||
id: sourceId,
|
||||
addOrUpdate,
|
||||
getAnnotation,
|
||||
} = csToolsVer4MeasurementSource;
|
||||
|
||||
/* Measurement Service Events */
|
||||
cornerstone.events.addEventListener(
|
||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
||||
event => {
|
||||
const {
|
||||
MEASUREMENT_ADDED,
|
||||
MEASUREMENT_UPDATED,
|
||||
} = measurementService.EVENTS;
|
||||
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
({ source, measurement }) => {
|
||||
if (![sourceId].includes(source.id)) {
|
||||
const annotation = getAnnotation('Length', measurement.id);
|
||||
|
||||
console.log(
|
||||
'Measurement Service [Cornerstone]: Measurement added',
|
||||
measurement
|
||||
);
|
||||
console.log('Mapped annotation:', annotation);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_UPDATED,
|
||||
({ source, measurement }) => {
|
||||
if (![sourceId].includes(source.id)) {
|
||||
const annotation = getAnnotation('Length', measurement.id);
|
||||
|
||||
console.log(
|
||||
'Measurement Service [Cornerstone]: Measurement updated',
|
||||
measurement
|
||||
);
|
||||
console.log('Mapped annotation:', annotation);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const addOrUpdateMeasurement = csToolsAnnotation => {
|
||||
try {
|
||||
const { toolName, toolType, measurementData } = csToolsAnnotation;
|
||||
const csTool = toolName || measurementData.toolType || toolType;
|
||||
csToolsAnnotation.id = measurementData._measurementServiceId;
|
||||
const measurementServiceId = addOrUpdate(csTool, csToolsAnnotation);
|
||||
|
||||
if (!measurementData._measurementServiceId) {
|
||||
addMeasurementServiceId(measurementServiceId, csToolsAnnotation);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to add or update measurement:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addMeasurementServiceId = (id, csToolsAnnotation) => {
|
||||
const { measurementData } = csToolsAnnotation;
|
||||
Object.assign(measurementData, { _measurementServiceId: id });
|
||||
};
|
||||
|
||||
[
|
||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
||||
].forEach(csToolsEvtName => {
|
||||
event.detail.element.addEventListener(
|
||||
csToolsEvtName,
|
||||
({ detail: csToolsAnnotation }) => {
|
||||
console.log(`Cornerstone Element Event: ${csToolsEvtName}`);
|
||||
addOrUpdateMeasurement(csToolsAnnotation);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -215,15 +215,6 @@ const definitions = [
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
label: 'Eraser',
|
||||
icon: 'eraser',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Eraser' },
|
||||
},
|
||||
{
|
||||
id: 'Download',
|
||||
label: 'Download',
|
||||
@@ -245,7 +236,7 @@ const definitions = [
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'setCornerstoneLayout',
|
||||
context: 'ACTIVE_VIEWPORT::VTK',
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const SUPPORTED_TOOLS = [
|
||||
'Length',
|
||||
'EllipticalRoi',
|
||||
'RectangleRoi',
|
||||
'ArrowAnnotate',
|
||||
];
|
||||
|
||||
const measurementServiceMappingsFactory = measurementService => {
|
||||
/**
|
||||
* Maps measurement service format object to cornerstone annotation object.
|
||||
*
|
||||
* @param {Measurement} measurement The measurement instance
|
||||
* @param {string} definition The source definition
|
||||
* @return {Object} Cornerstone annotation data
|
||||
*/
|
||||
const toAnnotation = (measurement, definition) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
points,
|
||||
unit,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID,
|
||||
} = measurement;
|
||||
|
||||
return {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
sopInstanceUid: SOPInstanceUID,
|
||||
frameOfReferenceUID: FrameOfReferenceUID,
|
||||
SeriesInstanceUID: referenceSeriesUID,
|
||||
unit,
|
||||
text: label,
|
||||
description,
|
||||
handles: _getHandlesFromPoints(points),
|
||||
_measurementServiceId: id,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
const toMeasurement = csToolsAnnotation => {
|
||||
const { element, measurementData } = csToolsAnnotation;
|
||||
const tool =
|
||||
csToolsAnnotation.toolType ||
|
||||
csToolsAnnotation.toolName ||
|
||||
measurementData.toolType;
|
||||
|
||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType(tool)) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
} = _getAttributes(element);
|
||||
|
||||
const points = [];
|
||||
points.push(measurementData.handles);
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
label: measurementData.text,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
area:
|
||||
measurementData.cachedStats &&
|
||||
measurementData.cachedStats
|
||||
.area /* TODO: Add concept names instead (descriptor) */,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points: _getPointsFromHandles(measurementData.handles),
|
||||
};
|
||||
};
|
||||
|
||||
const _getAttributes = element => {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const imageId = enabledElement.image.imageId;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
|
||||
return {
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
};
|
||||
};
|
||||
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;
|
||||
|
||||
/* TODO: Relocate static value types */
|
||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||
Length: POLYLINE,
|
||||
EllipticalRoi: ELLIPSE,
|
||||
RectangleRoi: POLYLINE,
|
||||
ArrowAnnotate: POINT,
|
||||
};
|
||||
|
||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||
};
|
||||
|
||||
const _getPointsFromHandles = handles => {
|
||||
let points = [];
|
||||
Object.keys(handles).map(handle => {
|
||||
if (['start', 'end'].includes(handle)) {
|
||||
let point = {};
|
||||
if (handles[handle].x) point.x = handles[handle].x;
|
||||
if (handles[handle].y) point.y = handles[handle].y;
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
return points;
|
||||
};
|
||||
|
||||
const _getHandlesFromPoints = points => {
|
||||
return points
|
||||
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
||||
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
||||
};
|
||||
|
||||
return {
|
||||
toAnnotation,
|
||||
toMeasurement,
|
||||
};
|
||||
};
|
||||
|
||||
export default measurementServiceMappingsFactory;
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';
|
||||
|
||||
jest.mock('cornerstone-core', () => ({
|
||||
...jest.requireActual('cornerstone-core'),
|
||||
getEnabledElement: () => ({
|
||||
image: { imageId: 123 },
|
||||
}),
|
||||
metaData: {
|
||||
...jest.requireActual('cornerstone-core').metaData,
|
||||
get: () => ({
|
||||
SOPInstanceUID: '123',
|
||||
FrameOfReferenceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('measurementServiceMappings.js', () => {
|
||||
let mappings;
|
||||
let handles;
|
||||
let points;
|
||||
let csToolsAnnotation;
|
||||
let measurement;
|
||||
let measurementServiceMock;
|
||||
let definition = 'Length';
|
||||
|
||||
beforeEach(() => {
|
||||
measurementServiceMock = {
|
||||
VALUE_TYPES: {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
},
|
||||
};
|
||||
mappings = measurementServiceMappingsFactory(measurementServiceMock);
|
||||
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
|
||||
points = [{ x: 1, y: 2 }, { x: 1, y: 2 }];
|
||||
csToolsAnnotation = {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
_measurementServiceId: 1,
|
||||
sopInstanceUid: '123',
|
||||
frameOfReferenceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
handles,
|
||||
text: 'Test',
|
||||
description: 'Test',
|
||||
unit: 'mm',
|
||||
},
|
||||
};
|
||||
measurement = {
|
||||
id: 1,
|
||||
SOPInstanceUID: '123',
|
||||
FrameOfReferenceUID: '123',
|
||||
referenceSeriesUID: '123',
|
||||
label: 'Test',
|
||||
description: 'Test',
|
||||
unit: 'mm',
|
||||
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
|
||||
points: points,
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('toAnnotation()', () => {
|
||||
it('map measurement service format to annotation', async () => {
|
||||
const mappedMeasurement = await mappings.toAnnotation(
|
||||
measurement,
|
||||
definition
|
||||
);
|
||||
expect(mappedMeasurement).toEqual(csToolsAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMeasurement()', () => {
|
||||
it('map annotation to measurement service format', async () => {
|
||||
const mappedAnnotation = await mappings.toMeasurement(csToolsAnnotation);
|
||||
expect(mappedAnnotation).toEqual(measurement);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.0...@ohif/extension-dicom-html@1.1.1) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"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.8.2",
|
||||
"dcmjs": "^0.10.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -11,26 +11,26 @@ const SOP_CLASS_UIDS = {
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||
};
|
||||
|
||||
const sopClassUids = Object.values(SOP_CLASS_UIDS);
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class
|
||||
const OHIFDicomHtmlSopClassHandler = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.7...@ohif/extension-dicom-microscopy@0.50.8) (2020-03-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.6...@ohif/extension-dicom-microscopy@0.50.7) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.5...@ohif/extension-dicom-microscopy@0.50.6) (2019-09-26)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.50.6",
|
||||
"version": "0.50.8",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -8,7 +8,7 @@ const SOP_CLASS_UIDS = {
|
||||
|
||||
const DicomMicroscopySopClassHandler = {
|
||||
id: 'DicomMicroscopySopClassHandlerPlugin',
|
||||
sopClassUids: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
sopClassUIDs: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
@@ -16,12 +16,12 @@ const DicomMicroscopySopClassHandler = {
|
||||
|
||||
return {
|
||||
plugin: 'microscopy',
|
||||
modality: 'SM',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'SM',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
dicomWebClient,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ class DicomMicroscopyViewport extends Component {
|
||||
const dicomWebClient = displaySet.dicomWebClient;
|
||||
|
||||
const searchInstanceOptions = {
|
||||
studyInstanceUID: displaySet.studyInstanceUid,
|
||||
seriesInstanceUID: displaySet.seriesInstanceUid,
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
};
|
||||
|
||||
dicomWebClient
|
||||
@@ -35,17 +35,18 @@ class DicomMicroscopyViewport extends Component {
|
||||
const promises = [];
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const sopInstanceUID = instances[i]['00080018']['Value'][0];
|
||||
|
||||
const retrieveInstanceOptions = {
|
||||
studyInstanceUID: displaySet.studyInstanceUid,
|
||||
seriesInstanceUID: displaySet.seriesInstanceUid,
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
};
|
||||
|
||||
const promise = dicomWebClient
|
||||
.retrieveInstanceMetadata(retrieveInstanceOptions)
|
||||
.then(metadata => {
|
||||
const imageType = metadata[0]['00080008']['Value'];
|
||||
if (imageType[2] === 'VOLUME') {
|
||||
const ImageType = metadata[0]['00080008']['Value'];
|
||||
if (ImageType[2] === 'VOLUME') {
|
||||
return metadata[0];
|
||||
}
|
||||
});
|
||||
@@ -56,13 +57,15 @@ class DicomMicroscopyViewport extends Component {
|
||||
.then(async metadata => {
|
||||
metadata = metadata.filter(m => m);
|
||||
|
||||
const { api } = await import(/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer');
|
||||
const { api } = await import(
|
||||
/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer'
|
||||
);
|
||||
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
||||
|
||||
this.viewer = new microscopyViewer({
|
||||
client: dicomWebClient,
|
||||
metadata,
|
||||
retrieveRendered: false
|
||||
retrieveRendered: false,
|
||||
});
|
||||
|
||||
this.viewer.render({ container });
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.1...@ohif/extension-dicom-pdf@1.0.2) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.0...@ohif/extension-dicom-pdf@1.0.1) (2019-12-16)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -101,9 +101,9 @@ class DicomPDFViewport extends Component {
|
||||
let pdfByteArray = byteArray;
|
||||
|
||||
if (dataSet) {
|
||||
const sopClassUid = dataSet.string('x00080016');
|
||||
const SOPClassUID = dataSet.string('x00080016');
|
||||
|
||||
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||
if (SOPClassUID !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||
throw new Error('This is not a DICOM-encapsulated PDF');
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,19 @@ const SOP_CLASS_UIDS = {
|
||||
const OHIFDicomPDFSopClassHandler = {
|
||||
id: 'OHIFDicomPDFSopClassHandlerPlugin',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
|
||||
sopClassUIDs: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'pdf',
|
||||
modality: 'DOC',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'DOC',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders: authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ class OHIFDicomPDFViewport extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { displaySet, studies } = this.props.viewportData;
|
||||
|
||||
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
|
||||
data => this.setState({ byteArray: new Uint8Array(data) }),
|
||||
error => {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtDicomSeg',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.0...@ohif/extension-dicom-segmentation@0.1.1) (2020-04-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* segmentation not loading ([#1566](https://github.com/OHIF/Viewers/issues/1566)) ([4a7ce1c](https://github.com/OHIF/Viewers/commit/4a7ce1c09324d74c61048393e3a2427757e4001a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 0.1.0 (2020-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,65 @@
|
||||
# @ohif/dicom-segmentation
|
||||
|
||||

|
||||
|
||||
This extension adds support to view and navigate the segments available in the DICOM SEG,
|
||||
allowing control over visibility of a given segment and providing metadata about the segment(s).
|
||||
|
||||
<!-- TODO: Simple image or GIF? -->
|
||||
|
||||
#### Index
|
||||
|
||||
Extension Id: `com.ohif.dicom-segmentation`
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
Tools can be configured through extension configuration using the tools key:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
tools: {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
## Annotate Tools Configuration
|
||||
|
||||
*We currently support one property for annotation tools.*
|
||||
|
||||
### Hide handles
|
||||
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
hideHandles: true,
|
||||
},
|
||||
...
|
||||
|
||||
## Resources
|
||||
|
||||
### Repositories
|
||||
|
||||
- [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
||||
- [cornerstonejs/cornerstoneTools][cornerstone-tools]
|
||||
- [cornerstonejs/cornerstone][cornerstone]
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[docs-commands]: https://www.com
|
||||
[docs-hotkeys]: https://www.com
|
||||
[docs-userprefs]: htt
|
||||
[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport
|
||||
[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools
|
||||
[cornerstone]: https://github.com/cornerstonejs/cornerstone
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.1.1",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:dicom-html": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"dcmjs": "^0.6.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { MODULE_TYPES, utils } from '@ohif/core';
|
||||
import loadSegmentation from './loadSegmentation';
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
DICOM_SEG: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
};
|
||||
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class.
|
||||
const OHIFDicomSegSopClassHandler = {
|
||||
id: 'OHIFDicomSegSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries: function (
|
||||
series,
|
||||
study,
|
||||
dicomWebClient,
|
||||
authorizationHeaders
|
||||
) {
|
||||
const instance = series.getFirstInstance();
|
||||
const metadata = instance.getData().metadata;
|
||||
|
||||
const {
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesDescription,
|
||||
FrameOfReferenceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = metadata;
|
||||
|
||||
const segDisplaySet = {
|
||||
Modality: 'SEG',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
authorizationHeaders,
|
||||
metadata,
|
||||
isDerived: true,
|
||||
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||
labelmapIndex: null, // Assigned when loaded.
|
||||
isLoaded: false,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesDescription,
|
||||
};
|
||||
|
||||
segDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
return loadSegmentation(
|
||||
segDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
).catch(error => {
|
||||
segDisplaySet.isLoaded = false;
|
||||
throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return segDisplaySet;
|
||||
},
|
||||
};
|
||||
|
||||
export default OHIFDicomSegSopClassHandler;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
.dcmseg-brush-color-selector .selector-buttons button {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
padding: 4px 12px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-buttons button:hover,
|
||||
.dcmseg-brush-color-selector .selector-buttons button:active,
|
||||
.dcmseg-brush-color-selector .selector-buttons button.active {
|
||||
background-color: var(--active-color);
|
||||
border-color: var(--ui-border-color-active);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-buttons {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-buttons button:last-of-type {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dcmseg-brush-color-selector .selector-active-segment {
|
||||
border-radius: 100%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './BrushColorSelector.css';
|
||||
|
||||
const BrushColorSelector = ({ defaultColor, index, onNext, onPrev }) => (
|
||||
<div className="dcmseg-brush-color-selector">
|
||||
<div
|
||||
className="selector-active-segment"
|
||||
style={{ backgroundColor: defaultColor }}
|
||||
>
|
||||
{index}
|
||||
</div>
|
||||
<div className="selector-buttons">
|
||||
<button onClick={onPrev}>
|
||||
Previous
|
||||
</button>
|
||||
<button onClick={onNext}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
BrushColorSelector.propTypes = {
|
||||
defaultColor: PropTypes.string.isRequired,
|
||||
index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
onPrev: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default BrushColorSelector;
|
||||
@@ -0,0 +1,4 @@
|
||||
.dcmseg-brush-radius label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Range } from '@ohif/ui';
|
||||
|
||||
import './BrushRadius.css';
|
||||
|
||||
const BrushRadius = ({ value, onChange, min, max, step }) => (
|
||||
<div className="dcmseg-brush-radius">
|
||||
<label htmlFor="dcmseg-brush-radius">Brush Radius</label>
|
||||
<Range
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onChange={onChange}
|
||||
id="dcmseg-brush-radius"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
BrushRadius.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
onChange: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
step: PropTypes.number,
|
||||
};
|
||||
|
||||
BrushRadius.defaultProps = {
|
||||
onChange: () => { },
|
||||
min: 1,
|
||||
max: 50,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
export default BrushRadius;
|
||||
@@ -0,0 +1,64 @@
|
||||
.dcmseg-segment-item .segment-color-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-color {
|
||||
margin-top: 8px;
|
||||
border-radius: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-label {
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 50px);
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-info {
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
padding-left: 9px;
|
||||
width: 90px;
|
||||
border-left: 1px solid var(--text-secondary-color);
|
||||
color: var(--text-primary-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item.selected .segment-actions {
|
||||
height: 35px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions {
|
||||
margin-left: -1px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
visibility: hidden;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions .btnAction {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
line-height: 35px;
|
||||
height: 35px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions .btnAction:hover, .dcmseg-segment-item .segment-actions .btnAction:active {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.dcmseg-segment-item .segment-actions .btnAction i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableListItem, Icon } from '@ohif/ui';
|
||||
|
||||
import './SegmentItem.css';
|
||||
|
||||
const ColoredCircle = ({ color }) => {
|
||||
return (
|
||||
<div
|
||||
className="segment-color"
|
||||
style={{ backgroundColor: `rgba(${color.join(',')})` }}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
ColoredCircle.propTypes = {
|
||||
color: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const SegmentItem = ({ index, label, onClick, itemClass, color }) => (
|
||||
<div className="dcmseg-segment-item">
|
||||
<TableListItem
|
||||
key={index}
|
||||
itemKey={index}
|
||||
itemIndex={index}
|
||||
itemClass={itemClass}
|
||||
itemMeta={<ColoredCircle color={color} />}
|
||||
itemMetaClass="segment-color-section"
|
||||
onItemClick={onClick}
|
||||
>
|
||||
<div>
|
||||
<div className="segment-label" style={{ marginBottom: 4 }}>
|
||||
{label}
|
||||
</div>
|
||||
{false && <div className="segment-info">{'...'}</div>}
|
||||
{false && (
|
||||
<div className="segment-actions">
|
||||
<button
|
||||
className="btnAction"
|
||||
onClick={() => console.log('Relabelling...')}
|
||||
>
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
Relabel
|
||||
</button>
|
||||
<button
|
||||
className="btnAction"
|
||||
onClick={() => console.log('Editing description...')}
|
||||
>
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
Description
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableListItem>
|
||||
</div>
|
||||
);
|
||||
|
||||
SegmentItem.propTypes = {
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
itemClass: PropTypes.string,
|
||||
color: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
SegmentItem.defaultProps = {
|
||||
itemClass: '',
|
||||
onClick: () => { }
|
||||
};
|
||||
|
||||
export default SegmentItem;
|
||||
@@ -0,0 +1,25 @@
|
||||
.dcmseg-segmentation-item {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-item .segmentation-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-item .segmentation-meta-title {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
max-width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-item .segmentation-meta-description {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './SegmentationItem.css';
|
||||
|
||||
const SegmentationItem = ({ onClick, title, description }) => {
|
||||
return (
|
||||
<li className="dcmseg-segmentation-item" onClick={onClick}>
|
||||
<div className="segmentation-meta">
|
||||
<div className="segmentation-meta-title">{title}</div>
|
||||
<div className="segmentation-meta-description">{description}</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentationItem.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
SegmentationItem.defaultProps = {
|
||||
description: ''
|
||||
};
|
||||
|
||||
export default SegmentationItem;
|
||||
@@ -0,0 +1,61 @@
|
||||
.dcmseg-segmentation-panel {
|
||||
color: white;
|
||||
min-height: 200px;
|
||||
background-color: var(--primary-background-color);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel h3 {
|
||||
padding-left: 20px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .segmentations {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .cog-icon {
|
||||
margin: 20px 20px 0 0;
|
||||
align-self: flex-end;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .cog-icon:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .segmentation-icon {
|
||||
margin-right: 8px;
|
||||
margin-top: 12px;
|
||||
min-width: 14px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .selector-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .selector-form>div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .selector-form>div:first-of-type {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .tableList .tableListHeader .numberOfItems {
|
||||
float: unset;
|
||||
max-width: unset;
|
||||
flex: unset;
|
||||
margin-right: 16px;
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import moment from 'moment';
|
||||
import { utils, log } from '@ohif/core';
|
||||
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||
|
||||
import {
|
||||
BrushColorSelector,
|
||||
BrushRadius,
|
||||
SegmentationItem,
|
||||
SegmentItem,
|
||||
SegmentationSelect,
|
||||
} from '../index';
|
||||
|
||||
import './SegmentationPanel.css';
|
||||
import SegmentationSettings from '../SegmentationSettings/SegmentationSettings';
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const refreshViewport = () => {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* SegmentationPanel component
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.studies
|
||||
* @param {Array} props.viewports - viewportSpecificData
|
||||
* @param {number} props.activeIndex - activeViewportIndex
|
||||
* @returns component
|
||||
*/
|
||||
const SegmentationPanel = ({ studies, viewports, activeIndex, isOpen }) => {
|
||||
/*
|
||||
* TODO: wrap get/set interactions with the cornerstoneTools
|
||||
* store with context to make these kind of things less blurry.
|
||||
*/
|
||||
const { configuration } = cornerstoneTools.getModule('segmentation');
|
||||
const DEFAULT_BRUSH_RADIUS = configuration.radius || 10;
|
||||
const [state, setState] = useState({
|
||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
||||
brushColor: 'rgba(221, 85, 85, 1)', /* TODO: We shouldn't hardcode this color, in the future the SEG may set the colorLUT to whatever it wants. */
|
||||
selectedSegment: null,
|
||||
selectedSegmentation: null,
|
||||
showSegSettings: false,
|
||||
brushStackState: null,
|
||||
labelmapList: [],
|
||||
segmentList: []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const labelmapModifiedHandler = event => {
|
||||
log.warn('Segmentation Panel: labelmap modified', event);
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const firstImageId = studyMetadata.getFirstImageId(activeViewport.displaySetInstanceUID);
|
||||
updateState('brushStackState', module.state.series[firstImageId]);
|
||||
};
|
||||
|
||||
/*
|
||||
* These are specific to each element;
|
||||
* Need to iterate cornerstone-tools tracked enabled elements?
|
||||
* Then only care about the one tied to active viewport?
|
||||
*/
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.addEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
labelmapModifiedHandler
|
||||
)
|
||||
);
|
||||
|
||||
return () => {
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.removeEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
labelmapModifiedHandler
|
||||
)
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
||||
const firstImageId = studyMetadata.getFirstImageId(activeViewport.displaySetInstanceUID);
|
||||
const brushStackState = module.state.series[firstImageId];
|
||||
|
||||
if (brushStackState) {
|
||||
const labelmap3D = brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
const labelmapList = getLabelmapList(brushStackState, firstImageId, activeViewport);
|
||||
const segmentList = getSegmentList(labelmap3D, firstImageId);
|
||||
setState(state => ({
|
||||
...state,
|
||||
brushStackState,
|
||||
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
||||
labelmapList,
|
||||
segmentList
|
||||
}));
|
||||
} else {
|
||||
setState(state => ({
|
||||
...state,
|
||||
labelmapList: [],
|
||||
segmentList: [],
|
||||
}));
|
||||
}
|
||||
}, [studies, viewports, activeIndex]);
|
||||
|
||||
/* Handle open/closed panel behaviour */
|
||||
useEffect(() => {
|
||||
updateState('showSegSettings', state.showSegSettings && !isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const getLabelmapList = (brushStackState, firstImageId, activeViewport) => {
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
activeViewport.StudyInstanceUID,
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const isActiveLabelmap =
|
||||
labelmapIndex === brushStackState.activeLabelmapIndex;
|
||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||
const displayTime = date.format('h:mm:ss a');
|
||||
const displayDescription = displaySet.SeriesDescription;
|
||||
|
||||
return {
|
||||
value: labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await _setActiveLabelmap(
|
||||
activeViewport,
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
brushStackState.activeLabelmapIndex
|
||||
);
|
||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const getSegmentList = (labelmap3D, firstImageId) => {
|
||||
/*
|
||||
* Newly created segments have no `meta`
|
||||
* So we instead build a list of all segment indexes in use
|
||||
* Then find any associated metadata
|
||||
*/
|
||||
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
||||
.reduce((acc, labelmap2D) => {
|
||||
if (labelmap2D) {
|
||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||
|
||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||
acc.push(segmentIndexes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable =
|
||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
||||
|
||||
const segmentList = [];
|
||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
||||
const segmentIndex = uniqueSegmentIndexes[i];
|
||||
|
||||
const color = colorLutTable[segmentIndex];
|
||||
let segmentLabel = '(unlabeled)';
|
||||
let segmentNumber = segmentIndex;
|
||||
|
||||
/* Meta */
|
||||
if (hasLabelmapMeta) {
|
||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
||||
|
||||
if (segmentMeta) {
|
||||
segmentNumber = segmentMeta.SegmentNumber;
|
||||
segmentLabel = segmentMeta.SegmentLabel;
|
||||
}
|
||||
}
|
||||
|
||||
const sameSegment = state.selectedSegment === segmentNumber;
|
||||
const setCurrentSelectedSegment = () => {
|
||||
_setActiveSegment(
|
||||
firstImageId,
|
||||
segmentNumber,
|
||||
labelmap3D.activeSegmentIndex
|
||||
);
|
||||
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
||||
};
|
||||
|
||||
segmentList.push(
|
||||
<SegmentItem
|
||||
key={segmentNumber}
|
||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||
onClick={setCurrentSelectedSegment}
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return segmentList;
|
||||
|
||||
/*
|
||||
* Let's iterate over segmentIndexes ^ above
|
||||
* If meta has a match, use it to show info
|
||||
* If now, add "no-meta" class
|
||||
* Show default name
|
||||
*/
|
||||
};
|
||||
|
||||
const updateState = (field, value) => {
|
||||
setState(state => ({ ...state, [field]: value }));
|
||||
};
|
||||
|
||||
const updateBrushSize = evt => {
|
||||
const updatedRadius = Number(evt.target.value);
|
||||
|
||||
if (updatedRadius !== brushRadius) {
|
||||
updateState('brushRadius', updatedRadius);
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
module.setters.radius(updatedRadius);
|
||||
}
|
||||
};
|
||||
|
||||
const decrementSegment = event => {
|
||||
event.preventDefault();
|
||||
if (labelmap3D.activeSegmentIndex > 1) {
|
||||
labelmap3D.activeSegmentIndex--;
|
||||
}
|
||||
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
|
||||
updateActiveSegmentColor();
|
||||
};
|
||||
|
||||
const incrementSegment = event => {
|
||||
event.preventDefault();
|
||||
labelmap3D.activeSegmentIndex++;
|
||||
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
|
||||
updateActiveSegmentColor();
|
||||
};
|
||||
|
||||
const updateActiveSegmentColor = () => {
|
||||
const color = getActiveSegmentColor();
|
||||
updateState('brushColor', color);
|
||||
};
|
||||
|
||||
const getActiveSegmentColor = () => {
|
||||
if (!state.brushStackState) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable =
|
||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
||||
|
||||
return `rgba(${color.join(',')})`;
|
||||
};
|
||||
|
||||
const updateConfiguration = newConfiguration => {
|
||||
/* Supported configuration */
|
||||
configuration.renderFill = newConfiguration.renderFill;
|
||||
configuration.renderOutline = newConfiguration.renderOutline;
|
||||
configuration.shouldRenderInactiveLabelmaps =
|
||||
newConfiguration.shouldRenderInactiveLabelmaps;
|
||||
configuration.fillAlpha = newConfiguration.fillAlpha;
|
||||
configuration.outlineAlpha = newConfiguration.outlineAlpha;
|
||||
configuration.outlineWidth = newConfiguration.outlineWidth;
|
||||
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
|
||||
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
|
||||
refreshViewport();
|
||||
};
|
||||
|
||||
if (state.showSegSettings) {
|
||||
return (
|
||||
<SegmentationSettings
|
||||
configuration={configuration}
|
||||
onBack={() => updateState('showSegSettings', false)}
|
||||
onChange={updateConfiguration}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="dcmseg-segmentation-panel">
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => updateState('showSegSettings', true)}
|
||||
/>
|
||||
{false && (
|
||||
<form className="selector-form">
|
||||
<BrushColorSelector
|
||||
defaultColor={brushColor}
|
||||
index={state.selectedSegment}
|
||||
onNext={incrementSegment}
|
||||
onPrev={decrementSegment}
|
||||
/>
|
||||
<BrushRadius
|
||||
value={brushRadius}
|
||||
onChange={updateBrushSize}
|
||||
min={configuration.minRadius}
|
||||
max={configuration.maxRadius}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
<h3>Segmentations</h3>
|
||||
<div className="segmentations">
|
||||
<SegmentationSelect
|
||||
value={
|
||||
state.labelmapList.find(i => i.value === state.selectedSegmentation) || null
|
||||
}
|
||||
formatOptionLabel={SegmentationItem}
|
||||
options={state.labelmapList}
|
||||
/>
|
||||
</div>
|
||||
<ScrollableArea>
|
||||
<TableList
|
||||
customHeader={<SegmentsHeader count={state.segmentList.length} />}
|
||||
>
|
||||
{state.segmentList}
|
||||
</TableList>
|
||||
</ScrollableArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
SegmentationPanel.propTypes = {
|
||||
/*
|
||||
* An object, with int index keys?
|
||||
* Maps to: state.viewports.viewportSpecificData, in `viewer`
|
||||
* Passed in MODULE_TYPES.PANEL when specifying component in viewer
|
||||
*/
|
||||
viewports: PropTypes.shape({
|
||||
displaySetInstanceUID: PropTypes.string,
|
||||
frameRate: PropTypes.any,
|
||||
InstanceNumber: PropTypes.number,
|
||||
isMultiFrame: PropTypes.bool,
|
||||
isReconstructable: PropTypes.bool,
|
||||
Modality: PropTypes.string,
|
||||
plugin: PropTypes.string,
|
||||
SeriesDate: PropTypes.string,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesInstanceUID: PropTypes.string,
|
||||
SeriesNumber: PropTypes.any,
|
||||
SeriesTime: PropTypes.string,
|
||||
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||
StudyInstanceUID: PropTypes.string,
|
||||
}),
|
||||
activeIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array.isRequired,
|
||||
};
|
||||
SegmentationPanel.defaultProps = {};
|
||||
|
||||
/**
|
||||
* Returns SEG Displaysets that reference the target series, sorted by dateTime
|
||||
*
|
||||
* @param {string} StudyInstanceUID
|
||||
* @param {string} SeriesInstanceUID
|
||||
* @returns Array
|
||||
*/
|
||||
const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
|
||||
/* Referenced DisplaySets */
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: SeriesInstanceUID,
|
||||
Modality: 'SEG',
|
||||
});
|
||||
|
||||
/* Sort */
|
||||
referencedDisplaysets.sort((a, b) => {
|
||||
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
|
||||
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
|
||||
return aNumber - bNumber;
|
||||
});
|
||||
|
||||
return referencedDisplaysets;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} viewportSpecificData
|
||||
* @param {*} studies
|
||||
* @param {*} displaySet
|
||||
* @param {*} firstImageId
|
||||
* @param {*} activeLabelmapIndex
|
||||
* @returns
|
||||
*/
|
||||
const _setActiveLabelmap = async (
|
||||
viewportSpecificData,
|
||||
studies,
|
||||
displaySet,
|
||||
firstImageId,
|
||||
activeLabelmapIndex
|
||||
) => {
|
||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||
return displaySet.labelmapIndex;
|
||||
}
|
||||
|
||||
if (!displaySet.isLoaded) {
|
||||
// What props does this expect `viewportSpecificData` to have?
|
||||
// TODO: Should this return the `labelmapIndex`?
|
||||
await displaySet.load(viewportSpecificData, studies);
|
||||
}
|
||||
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const brushStackState = state.series[firstImageId];
|
||||
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
|
||||
|
||||
refreshViewport();
|
||||
|
||||
return displaySet.labelmapIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} firstImageId
|
||||
* @param {*} activeSegmentIndex
|
||||
* @returns
|
||||
*/
|
||||
const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
|
||||
if (segmentIndex === activeSegmentIndex) {
|
||||
log.info(`${activeSegmentIndex} is already the active segment`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const brushStackState = state.series[firstImageId];
|
||||
|
||||
const labelmap3D =
|
||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
labelmap3D.activeSegmentIndex = segmentIndex;
|
||||
|
||||
refreshViewport();
|
||||
|
||||
return segmentIndex;
|
||||
};
|
||||
|
||||
const SegmentsHeader = ({ count }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="tableListHeaderTitle">Segments</div>
|
||||
<div className="numberOfItems">{count}</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default SegmentationPanel;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
|
||||
const SegmentationSelect = ({ value, formatOptionLabel, options }) => (
|
||||
<Select
|
||||
value={value}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={segmentationSelectStyles}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
|
||||
const computedstyle = getComputedStyle(document.body);
|
||||
const uiGrayDarker = computedstyle.getPropertyValue('--ui-gray-darker');
|
||||
const activeColor = computedstyle.getPropertyValue('--active-color');
|
||||
const defaultColor = computedstyle.getPropertyValue('--default-color');
|
||||
const uiGrayDark = computedstyle.getPropertyValue('--ui-gray-dark');
|
||||
|
||||
const segmentationSelectStyles = {
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
cursor: 'pointer',
|
||||
background: uiGrayDarker,
|
||||
borderRadius: state.isFocused ? '5px 5px 5px 5px' : 5,
|
||||
borderColor: state.isFocused ? activeColor : defaultColor,
|
||||
boxShadow: state.isFocused ? null : null,
|
||||
minHeight: '50px',
|
||||
'&:hover': {
|
||||
borderColor: activeColor,
|
||||
},
|
||||
}),
|
||||
menu: base => ({
|
||||
...base,
|
||||
borderRadius: 5,
|
||||
background: uiGrayDarker,
|
||||
}),
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
cursor: 'pointer',
|
||||
'&:first-of-type': {
|
||||
borderTopLeftRadius: 5,
|
||||
borderTopRightRadius: 5,
|
||||
},
|
||||
'&:last-of-type': {
|
||||
borderBottomLeftRadius: 5,
|
||||
borderBottomRightRadius: 5,
|
||||
},
|
||||
background: state.isSelected ? uiGrayDark : uiGrayDarker,
|
||||
'&:hover': {
|
||||
background: uiGrayDark,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export default SegmentationSelect;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
.dcmseg-segmentation-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .range label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-title h3 {
|
||||
color: var(--text-primary-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-group .range {
|
||||
display: flex;
|
||||
padding: 20px 15px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-group .range:not(:last-of-type) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-group .range label {
|
||||
margin: 0;
|
||||
display: unset;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-group .range input {
|
||||
width: 100%;
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .custom-check label {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid black;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .custom-check input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .settings-group {
|
||||
background-color: var(--ui-gray-dark);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .return-button {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
padding: 4px 12px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-settings .return-button:hover,
|
||||
.dcmseg-segmentation-settings .return-button:active,
|
||||
.dcmseg-segmentation-settings .return-button.active {
|
||||
background-color: var(--active-color);
|
||||
border-color: var(--ui-border-color-active);
|
||||
color: #fff;
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Range } from '@ohif/ui';
|
||||
|
||||
import './SegmentationSettings.css';
|
||||
|
||||
const SegmentationSettings = ({ configuration, onBack, onChange }) => {
|
||||
const [state, setState] = useState({
|
||||
renderFill: configuration.renderFill,
|
||||
renderOutline: configuration.renderOutline,
|
||||
shouldRenderInactiveLabelmaps: configuration.shouldRenderInactiveLabelmaps,
|
||||
fillAlpha: configuration.fillAlpha,
|
||||
outlineAlpha: configuration.outlineAlpha,
|
||||
outlineWidth: configuration.outlineWidth,
|
||||
fillAlphaInactive: configuration.fillAlphaInactive,
|
||||
outlineAlphaInactive: configuration.outlineAlphaInactive
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onChange(state);
|
||||
}, [state]);
|
||||
|
||||
const check = field => {
|
||||
setState(state => ({ ...state, [field]: !state[field] }));
|
||||
};
|
||||
|
||||
const save = (field, value) => {
|
||||
setState(state => ({ ...state, [field]: value }));
|
||||
};
|
||||
|
||||
const toFloat = value => parseFloat(value / 100).toFixed(2);
|
||||
|
||||
return (
|
||||
<div className="dcmseg-segmentation-settings">
|
||||
<div className="settings-title">
|
||||
<h3>Segmentations Settings</h3>
|
||||
<button className="return-button" onClick={onBack}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="settings-group"
|
||||
style={{ marginBottom: state.renderFill ? 15 : 0 }}
|
||||
>
|
||||
<CustomCheck
|
||||
label="Segment Fill"
|
||||
checked={state.renderFill}
|
||||
onChange={() => check('renderFill')}
|
||||
/>
|
||||
{state.renderFill && (
|
||||
<CustomRange
|
||||
label="Opacity"
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.fillAlpha * 100}
|
||||
onChange={event => save('fillAlpha', toFloat(event.target.value))}
|
||||
showPercentage
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="settings-group"
|
||||
style={{ marginBottom: state.renderOutline ? 15 : 0 }}
|
||||
>
|
||||
<CustomCheck
|
||||
label="Segment Outline"
|
||||
checked={state.renderOutline}
|
||||
onChange={() => check('renderOutline')}
|
||||
/>
|
||||
{state.renderOutline && (
|
||||
<>
|
||||
<CustomRange
|
||||
value={state.outlineAlpha * 100}
|
||||
label="Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={event => save('outlineAlpha', toFloat(event.target.value))}
|
||||
/>
|
||||
<CustomRange
|
||||
value={state.outlineWidth}
|
||||
label="Width"
|
||||
showValue
|
||||
step={1}
|
||||
min={0}
|
||||
max={5}
|
||||
onChange={event => save('outlineWidth', parseInt(event.target.value))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(state.renderFill || state.renderOutline) && (
|
||||
<div
|
||||
className="settings-group"
|
||||
style={{ marginBottom: state.shouldRenderInactiveLabelmaps ? 15 : 0 }}
|
||||
>
|
||||
<CustomCheck
|
||||
label="Render inactive segmentations"
|
||||
checked={state.shouldRenderInactiveLabelmaps}
|
||||
onChange={() => check('shouldRenderInactiveLabelmaps')}
|
||||
/>
|
||||
{state.shouldRenderInactiveLabelmaps && (
|
||||
<>
|
||||
{state.renderFill && (
|
||||
<CustomRange
|
||||
label="Fill Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.fillAlphaInactive * 100}
|
||||
onChange={event => save('fillAlphaInactive', toFloat(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
{state.renderOutline && (
|
||||
<CustomRange
|
||||
label="Outline Opacity"
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={state.outlineAlphaInactive * 100}
|
||||
onChange={event => save('outlineAlphaInactive', toFloat(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomCheck = ({ label, checked, onChange }) => {
|
||||
return (
|
||||
<div className="custom-check">
|
||||
<label>
|
||||
<span>{label}</span>
|
||||
<input type="checkbox" checked={checked} onChange={onChange} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomRange = props => {
|
||||
const { label, onChange } = props;
|
||||
return (
|
||||
<div className="range">
|
||||
<label htmlFor="range">{label}</label>
|
||||
<Range
|
||||
{...props}
|
||||
onChange={event => {
|
||||
event.persist();
|
||||
onChange(event);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SegmentationSettings.propTypes = {
|
||||
configuration: PropTypes.shape({
|
||||
renderFill: PropTypes.bool.isRequired,
|
||||
renderOutline: PropTypes.bool.isRequired,
|
||||
shouldRenderInactiveLabelmaps: PropTypes.bool.isRequired,
|
||||
fillAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, /* TODO: why fillAlpha is string? */
|
||||
outlineAlpha: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, /* TODO: why fillAlpha is string? */
|
||||
outlineWidth: PropTypes.number.isRequired,
|
||||
fillAlphaInactive: PropTypes.number.isRequired,
|
||||
outlineAlphaInactive: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SegmentationSettings;
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as BrushColorSelector } from './BrushColorSelector/BrushColorSelector';
|
||||
export { default as BrushRadius } from './BrushRadius/BrushRadius';
|
||||
export { default as SegmentationItem } from './SegmentationItem/SegmentationItem';
|
||||
export { default as SegmentationSettings } from './SegmentationSettings/SegmentationSettings';
|
||||
export { default as SegmentationPanel } from './SegmentationPanel/SegmentationPanel';
|
||||
export { default as SegmentItem } from './SegmentItem/SegmentItem';
|
||||
export { default as SegmentationSelect } from './SegmentationSelect';
|
||||
@@ -0,0 +1,30 @@
|
||||
import init from './init.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import panelModule from './panelModule.js';
|
||||
import sopClassHandlerModule from './OHIFDicomSegSopClassHandler.js';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'com.ohif.dicom-segmentation',
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getToolbarModule({ servicesManager }) {
|
||||
return toolbarModule;
|
||||
},
|
||||
getPanelModule({ servicesManager }) {
|
||||
return panelModule;
|
||||
},
|
||||
getSopClassHandlerModule({ servicesManager }) {
|
||||
return sopClassHandlerModule;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import csTools from 'cornerstone-tools';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { BrushTool, SphericalBrushTool, CorrectionScissorsTool } = csTools;
|
||||
const tools = [BrushTool, SphericalBrushTool, CorrectionScissorsTool];
|
||||
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
|
||||
csTools.addTool(BrushTool, {
|
||||
name: 'BrushEraser',
|
||||
configuration: {
|
||||
alwaysEraseOnClick: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { DicomLoaderService } = OHIF.utils;
|
||||
|
||||
export default async function loadSegmentation(
|
||||
segDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
) {
|
||||
const { StudyInstanceUID } = referencedDisplaySet;
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
segDisplaySet.isLoaded = true;
|
||||
|
||||
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
studies
|
||||
);
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const imageIds = _getImageIdsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
referencedDisplaySet.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const results = _parseSeg(segArrayBuffer, imageIds);
|
||||
|
||||
if (!results) {
|
||||
throw new Error('Fractional segmentations are not yet supported');
|
||||
}
|
||||
|
||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
// TODO: Could define a color LUT based on colors in the SEG.
|
||||
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
||||
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
labelmapIndex,
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame
|
||||
);
|
||||
|
||||
segDisplaySet.labelmapIndex = labelmapIndex;
|
||||
}
|
||||
|
||||
function _getNextLabelmapIndex(firstImageId) {
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const brushStackState = state.series[firstImageId];
|
||||
|
||||
let labelmapIndex = 0;
|
||||
|
||||
if (brushStackState) {
|
||||
const { labelmaps3D } = brushStackState;
|
||||
labelmapIndex = labelmaps3D.length;
|
||||
|
||||
for (let i = 0; i < labelmaps3D.length; i++) {
|
||||
if (!labelmaps3D[i]) {
|
||||
labelmapIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labelmapIndex;
|
||||
}
|
||||
|
||||
function _parseSeg(arrayBuffer, imageIds) {
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
cornerstone.metaData
|
||||
);
|
||||
}
|
||||
|
||||
function _getImageIdsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = study.displaySets.filter(displaySet => {
|
||||
return displaySet.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
// TODO -> We could make check the instance list and see if any match?
|
||||
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||
// ^ Will that even happen?
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySets[0];
|
||||
|
||||
return referencedDisplaySet.images.map(image => image.getImageId());
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js';
|
||||
|
||||
export default {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'Segmentations',
|
||||
target: 'segmentation-panel',
|
||||
isDisabled: studies => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < studies.length; i++) {
|
||||
const study = studies[i];
|
||||
|
||||
if (study && study.series) {
|
||||
for (let j = 0; j < study.series.length; j++) {
|
||||
const series = study.series[j];
|
||||
|
||||
if (series.Modality === 'SEG') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'segmentation-panel',
|
||||
component: SegmentationPanel,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
/* TODO: Creation tools (future release).
|
||||
|
||||
const TOOLBAR_BUTTON_TYPES = {
|
||||
COMMAND: 'command',
|
||||
SET_TOOL_ACTIVE: 'setToolActive',
|
||||
BUILT_IN: 'builtIn',
|
||||
};
|
||||
|
||||
*/
|
||||
|
||||
const definitions = []; /* TODO: Creation tools (future release). [
|
||||
{
|
||||
id: 'SegDropdown',
|
||||
label: 'Segmentation',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
id: 'Brush',
|
||||
label: 'Brush',
|
||||
icon: 'brush',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Brush' },
|
||||
},
|
||||
{
|
||||
id: 'SphericalBrush',
|
||||
label: 'Spherical',
|
||||
icon: 'sphere',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'SphericalBrush' },
|
||||
},
|
||||
{
|
||||
id: 'CorrectionScissors',
|
||||
label: 'Correction Scissors',
|
||||
icon: 'scissors',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'CorrectionScissors' },
|
||||
},
|
||||
{
|
||||
id: 'BrushEraser',
|
||||
label: 'Eraser',
|
||||
icon: 'trash',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'BrushEraser' },
|
||||
},
|
||||
],
|
||||
},
|
||||
]; */
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtLesionTracker',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-lesion-tracker@0.1.0...@ohif/extension-lesion-tracker@0.2.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 0.1.0 (2020-02-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,38 @@
|
||||
# @ohif/extension-lesion-tracker
|
||||
|
||||
This project is an OHIF extension that can be used with the Core OHIF Platform,
|
||||
either at runtime, or as an ES6 dependency, to create a medical image viewing
|
||||
application similar to that of the [legacy Lesion
|
||||
Tracker][legacy-lesion-tracker] viewer.
|
||||
|
||||
## About
|
||||
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
...
|
||||
|
||||
### Configuration
|
||||
|
||||
...
|
||||
|
||||
### Extensions
|
||||
|
||||
...
|
||||
|
||||
## Build & Deploy
|
||||
|
||||
...
|
||||
|
||||
## Funding/Support
|
||||
|
||||
...
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[legacy-lesion-tracker]: http://lesiontracker.ohif.org/studylist
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@ohif/extension-lesion-tracker",
|
||||
"version": "0.2.0",
|
||||
"description": "OHIF extension for Lesion Tracker",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:lesion-tracker": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"classnames": "^2.2.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.MeasurementComparisonTable {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .displayTexts {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .displayTexts .measurementDisplayText {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status {
|
||||
left: 0;
|
||||
top: 3px;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status .warning-border {
|
||||
padding: 2px 3px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status svg {
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .measurementTableHeaderItem {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .generate-report {
|
||||
background-color: #151a1f;
|
||||
margin-top: 2px;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .generate-report button {
|
||||
font-size: 12px;
|
||||
color: black;
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MeasurementTable } from '@ohif/ui';
|
||||
|
||||
import './MeasurementComparisonTable.css';
|
||||
|
||||
const overallWarnings = {
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
};
|
||||
|
||||
const measurements = [
|
||||
{
|
||||
measurementId: '125',
|
||||
measurementNumber: '125',
|
||||
itemNumber: 1,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '12.5 x 4.6' }],
|
||||
},
|
||||
{
|
||||
measurementId: '124',
|
||||
measurementNumber: '124',
|
||||
itemNumber: 2,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '32.5 x 1.6' }],
|
||||
},
|
||||
{
|
||||
measurementId: '123',
|
||||
measurementNumber: '123',
|
||||
itemNumber: 3,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '5.5 x 9.2' }],
|
||||
},
|
||||
];
|
||||
|
||||
const additionalFindings = [
|
||||
{
|
||||
measurementId: '122',
|
||||
measurementNumber: '122',
|
||||
itemNumber: 1,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '23.5 x 9.2' }],
|
||||
},
|
||||
{
|
||||
measurementId: '121',
|
||||
measurementNumber: '121',
|
||||
itemNumber: 2,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '11.2 x 9.2' }],
|
||||
},
|
||||
{
|
||||
measurementId: '120',
|
||||
measurementNumber: '120',
|
||||
itemNumber: 3,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '2.9 x 9.2' }],
|
||||
},
|
||||
];
|
||||
|
||||
const currentCollections = [
|
||||
{
|
||||
selectorAction: () => { },
|
||||
maxMeasurements: 3,
|
||||
groupName: 'Measurements',
|
||||
measurements: measurements,
|
||||
},
|
||||
{
|
||||
selectorAction: () => { },
|
||||
groupName: 'Additional Findings',
|
||||
measurements: additionalFindings,
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonColletions = [
|
||||
{
|
||||
selectorAction: () => { },
|
||||
maxMeasurements: 3,
|
||||
groupName: 'Measurements',
|
||||
measurements: measurements,
|
||||
},
|
||||
{
|
||||
selectorAction: () => { },
|
||||
groupName: 'Additional Findings',
|
||||
measurements: additionalFindings,
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonCollections = currentCollections.map((group, index) => {
|
||||
return {
|
||||
...group,
|
||||
measurements: group.measurements.map((measurement, measurementIndex) => {
|
||||
const comparisonCollection = comparisonColletions[index].measurements;
|
||||
if (measurementIndex < comparisonCollection.length) {
|
||||
return {
|
||||
...measurement,
|
||||
data: [
|
||||
...measurement.data,
|
||||
...comparisonCollection[measurementIndex].data,
|
||||
],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const comparisonTimepoints = [
|
||||
{
|
||||
key: 'Current',
|
||||
date: '10-Apr-18',
|
||||
},
|
||||
{
|
||||
key: 'Comparison',
|
||||
date: '15-Jun-18',
|
||||
}
|
||||
];
|
||||
|
||||
const MeasurementComparisonTable = () => {
|
||||
return (
|
||||
<div className="MeasurementComparisonTable">
|
||||
<MeasurementTable
|
||||
timepoints={comparisonTimepoints}
|
||||
overallWarnings={overallWarnings}
|
||||
measurementCollection={comparisonCollections}
|
||||
onRelabelClick={() => { }}
|
||||
onEditDescriptionClick={() => { }}
|
||||
/>
|
||||
<div className="generate-report">
|
||||
<button className="btn btn-primary">Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeasurementComparisonTable;
|
||||
@@ -0,0 +1,52 @@
|
||||
import MeasurementComparisonTable from './components/MeasurementComparisonTable';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'lesion-tracker',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'th-list',
|
||||
label: 'Measurements',
|
||||
target: 'lesion-tracker-panel',
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'lesion-tracker-panel',
|
||||
component: MeasurementComparisonTable,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER']
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns Object
|
||||
*/
|
||||
getToolbarModule() {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns Object
|
||||
*/
|
||||
getCommandsModule() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
+245
-319
@@ -3,6 +3,185 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.5...@ohif/extension-vtk@1.3.6) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.4...@ohif/extension-vtk@1.3.5) (2020-03-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.3...@ohif/extension-vtk@1.3.4) (2020-03-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.2...@ohif/extension-vtk@1.3.3) (2020-03-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.1...@ohif/extension-vtk@1.3.2) (2020-03-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.0...@ohif/extension-vtk@1.3.1) (2020-03-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.24...@ohif/extension-vtk@1.3.0) (2020-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.24](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.23...@ohif/extension-vtk@1.2.24) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.22...@ohif/extension-vtk@1.2.23) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.21...@ohif/extension-vtk@1.2.22) (2020-03-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.20...@ohif/extension-vtk@1.2.21) (2020-02-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.19...@ohif/extension-vtk@1.2.20) (2020-02-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.18...@ohif/extension-vtk@1.2.19) (2020-02-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.17...@ohif/extension-vtk@1.2.18) (2020-02-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.16...@ohif/extension-vtk@1.2.17) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.15...@ohif/extension-vtk@1.2.16) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.14...@ohif/extension-vtk@1.2.15) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.13...@ohif/extension-vtk@1.2.14) (2020-02-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.12...@ohif/extension-vtk@1.2.13) (2020-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.11...@ohif/extension-vtk@1.2.12) (2020-01-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.10...@ohif/extension-vtk@1.2.11) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.9...@ohif/extension-vtk@1.2.10) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
@@ -313,621 +492,368 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.4...@ohif/extension-vtk@0.53.5) (2019-11-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.3...@ohif/extension-vtk@0.53.4) (2019-11-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.2...@ohif/extension-vtk@0.53.3) (2019-11-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mpr2d vtkjs viewport does not render if range is set to `NaN` values ([#1157](https://github.com/OHIF/Viewers/issues/1157)) ([84a0972](https://github.com/OHIF/Viewers/commit/84a097212babc0f98198b9ced1def9743973a5a8))
|
||||
|
||||
|
||||
|
||||
|
||||
- mpr2d vtkjs viewport does not render if range is set to `NaN` values
|
||||
([#1157](https://github.com/OHIF/Viewers/issues/1157))
|
||||
([84a0972](https://github.com/OHIF/Viewers/commit/84a097212babc0f98198b9ced1def9743973a5a8))
|
||||
|
||||
## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.1...@ohif/extension-vtk@0.53.2) (2019-11-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.0...@ohif/extension-vtk@0.53.1) (2019-11-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.32...@ohif/extension-vtk@0.53.0) (2019-11-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
|
||||
|
||||
|
||||
|
||||
|
||||
- modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151))
|
||||
([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)),
|
||||
closes [#1086](https://github.com/OHIF/Viewers/issues/1086)
|
||||
[#1116](https://github.com/OHIF/Viewers/issues/1116)
|
||||
[#1116](https://github.com/OHIF/Viewers/issues/1116)
|
||||
[#1146](https://github.com/OHIF/Viewers/issues/1146)
|
||||
[#1142](https://github.com/OHIF/Viewers/issues/1142)
|
||||
[#1143](https://github.com/OHIF/Viewers/issues/1143)
|
||||
[#1110](https://github.com/OHIF/Viewers/issues/1110)
|
||||
[#1086](https://github.com/OHIF/Viewers/issues/1086)
|
||||
[#1116](https://github.com/OHIF/Viewers/issues/1116)
|
||||
[#1119](https://github.com/OHIF/Viewers/issues/1119)
|
||||
|
||||
## [0.52.32](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.31...@ohif/extension-vtk@0.52.32) (2019-11-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.31](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.30...@ohif/extension-vtk@0.52.31) (2019-11-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
## [0.52.30](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.29...@ohif/extension-vtk@0.52.30) (2019-11-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.29](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.28...@ohif/extension-vtk@0.52.29) (2019-11-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Upgrade react-vtkjs-viewport to fix volume orientation ([#1143](https://github.com/OHIF/Viewers/issues/1143)) ([1d86b1a](https://github.com/OHIF/Viewers/commit/1d86b1a102fc422053cff103de8c12ca6f437354))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🐛 Upgrade react-vtkjs-viewport to fix volume orientation
|
||||
([#1143](https://github.com/OHIF/Viewers/issues/1143))
|
||||
([1d86b1a](https://github.com/OHIF/Viewers/commit/1d86b1a102fc422053cff103de8c12ca6f437354))
|
||||
|
||||
## [0.52.28](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.27...@ohif/extension-vtk@0.52.28) (2019-11-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.27](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.26...@ohif/extension-vtk@0.52.27) (2019-10-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.26](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.25...@ohif/extension-vtk@0.52.26) (2019-10-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.25](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.24...@ohif/extension-vtk@0.52.25) (2019-10-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.24](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.23...@ohif/extension-vtk@0.52.24) (2019-10-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.22...@ohif/extension-vtk@0.52.23) (2019-10-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.21...@ohif/extension-vtk@0.52.22) (2019-10-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109)) ([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760))
|
||||
|
||||
|
||||
|
||||
|
||||
- MIP styling ([#1109](https://github.com/OHIF/Viewers/issues/1109))
|
||||
([0d21cc6](https://github.com/OHIF/Viewers/commit/0d21cc6ad0c47706b9e62e05fe2a0f1d86339760))
|
||||
|
||||
## [0.52.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.20...@ohif/extension-vtk@0.52.21) (2019-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.19...@ohif/extension-vtk@0.52.20) (2019-10-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.18...@ohif/extension-vtk@0.52.19) (2019-10-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.17...@ohif/extension-vtk@0.52.18) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092))
|
||||
([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
## [0.52.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.16...@ohif/extension-vtk@0.52.17) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix crosshairs location when using MIP ([#1080](https://github.com/OHIF/Viewers/issues/1080)) ([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🐛 Fix crosshairs location when using MIP
|
||||
([#1080](https://github.com/OHIF/Viewers/issues/1080))
|
||||
([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
|
||||
|
||||
## [0.52.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.15...@ohif/extension-vtk@0.52.16) (2019-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Switch to orhtographic view for 2D MPR ([#1074](https://github.com/OHIF/Viewers/issues/1074)) ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🐛 Switch to orhtographic view for 2D MPR
|
||||
([#1074](https://github.com/OHIF/Viewers/issues/1074))
|
||||
([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
|
||||
|
||||
## [0.52.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.14...@ohif/extension-vtk@0.52.15) (2019-10-23)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* ⚡️ Throttle UI refresh of synced WL value in vtk port ([#1070](https://github.com/OHIF/Viewers/issues/1070)) ([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
|
||||
|
||||
|
||||
|
||||
|
||||
- ⚡️ Throttle UI refresh of synced WL value in vtk port
|
||||
([#1070](https://github.com/OHIF/Viewers/issues/1070))
|
||||
([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
|
||||
|
||||
## [0.52.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.13...@ohif/extension-vtk@0.52.14) (2019-10-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.12...@ohif/extension-vtk@0.52.13) (2019-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.10...@ohif/extension-vtk@0.52.11) (2019-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.9...@ohif/extension-vtk@0.52.10) (2019-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.8...@ohif/extension-vtk@0.52.9) (2019-10-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.7...@ohif/extension-vtk@0.52.8) (2019-10-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.6...@ohif/extension-vtk@0.52.7) (2019-10-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.5...@ohif/extension-vtk@0.52.6) (2019-10-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.4...@ohif/extension-vtk@0.52.5) (2019-10-10)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* ⚡️ Update react-vtkjs-viewport for performance improvement ([6ee5094](https://github.com/OHIF/Viewers/commit/6ee5094))
|
||||
|
||||
|
||||
|
||||
|
||||
- ⚡️ Update react-vtkjs-viewport for performance improvement
|
||||
([6ee5094](https://github.com/OHIF/Viewers/commit/6ee5094))
|
||||
|
||||
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.3...@ohif/extension-vtk@0.52.4) (2019-10-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.2...@ohif/extension-vtk@0.52.3) (2019-10-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.1...@ohif/extension-vtk@0.52.2) (2019-10-09)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 🎸 Update vtk viewport for faster image reconstruction ([#1016](https://github.com/OHIF/Viewers/issues/1016)) ([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Update vtk viewport for faster image reconstruction
|
||||
([#1016](https://github.com/OHIF/Viewers/issues/1016))
|
||||
([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
|
||||
|
||||
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.0...@ohif/extension-vtk@0.52.1) (2019-10-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.3...@ohif/extension-vtk@0.52.0) (2019-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Synced Window Leveling ([559b637](https://github.com/OHIF/Viewers/commit/559b637)), closes [#558](https://github.com/OHIF/Viewers/issues/558)
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Synced Window Leveling
|
||||
([559b637](https://github.com/OHIF/Viewers/commit/559b637)), closes
|
||||
[#558](https://github.com/OHIF/Viewers/issues/558)
|
||||
|
||||
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.2...@ohif/extension-vtk@0.51.3) (2019-10-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.1...@ohif/extension-vtk@0.51.2) (2019-10-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.0...@ohif/extension-vtk@0.51.1) (2019-10-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2 ([#964](https://github.com/OHIF/Viewers/issues/964)) ([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
|
||||
|
||||
|
||||
|
||||
|
||||
- **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2
|
||||
([#964](https://github.com/OHIF/Viewers/issues/964))
|
||||
([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
|
||||
|
||||
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.23...@ohif/extension-vtk@0.51.0) (2019-10-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode
|
||||
toggle ([#947](https://github.com/OHIF/Viewers/issues/947))
|
||||
([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
|
||||
|
||||
## [0.50.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.22...@ohif/extension-vtk@0.50.23) (2019-10-01)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.21...@ohif/extension-vtk@0.50.22) (2019-09-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.20...@ohif/extension-vtk@0.50.21) (2019-09-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.19...@ohif/extension-vtk@0.50.20) (2019-09-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.18...@ohif/extension-vtk@0.50.19) (2019-09-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add some code splitting for PWA build ([#937](https://github.com/OHIF/Viewers/issues/937)) ([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||
|
||||
|
||||
|
||||
|
||||
- Add some code splitting for PWA build
|
||||
([#937](https://github.com/OHIF/Viewers/issues/937))
|
||||
([8938035](https://github.com/OHIF/Viewers/commit/8938035))
|
||||
|
||||
## [0.50.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.17...@ohif/extension-vtk@0.50.18) (2019-09-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.16...@ohif/extension-vtk@0.50.17) (2019-09-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.15...@ohif/extension-vtk@0.50.16) (2019-09-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Set transfer function range to data range when loading volumes in VTK ([#930](https://github.com/OHIF/Viewers/issues/930)) ([2341b32](https://github.com/OHIF/Viewers/commit/2341b32))
|
||||
|
||||
|
||||
|
||||
|
||||
- Set transfer function range to data range when loading volumes in VTK
|
||||
([#930](https://github.com/OHIF/Viewers/issues/930))
|
||||
([2341b32](https://github.com/OHIF/Viewers/commit/2341b32))
|
||||
|
||||
## [0.50.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.14...@ohif/extension-vtk@0.50.15) (2019-09-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Bump VTK viewport dependency ([#929](https://github.com/OHIF/Viewers/issues/929)) ([66a0aad](https://github.com/OHIF/Viewers/commit/66a0aad))
|
||||
* Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools are implemented ([#926](https://github.com/OHIF/Viewers/issues/926)) ([7f642a0](https://github.com/OHIF/Viewers/commit/7f642a0))
|
||||
|
||||
|
||||
|
||||
|
||||
- Bump VTK viewport dependency
|
||||
([#929](https://github.com/OHIF/Viewers/issues/929))
|
||||
([66a0aad](https://github.com/OHIF/Viewers/commit/66a0aad))
|
||||
- Stop adding empty labelmap to VTK Viewports for now. Wait until paint tools
|
||||
are implemented ([#926](https://github.com/OHIF/Viewers/issues/926))
|
||||
([7f642a0](https://github.com/OHIF/Viewers/commit/7f642a0))
|
||||
|
||||
## [0.50.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.13...@ohif/extension-vtk@0.50.14) (2019-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||
|
||||
|
||||
|
||||
|
||||
- bump cornerstone-tools version in peerDeps
|
||||
([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||
|
||||
## [0.50.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.12...@ohif/extension-vtk@0.50.13) (2019-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.11...@ohif/extension-vtk@0.50.12) (2019-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.10...@ohif/extension-vtk@0.50.11) (2019-09-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* simplify runtime-extension usage ([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
|
||||
|
||||
|
||||
|
||||
|
||||
- simplify runtime-extension usage
|
||||
([ac5dbda](https://github.com/OHIF/Viewers/commit/ac5dbda))
|
||||
|
||||
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.9...@ohif/extension-vtk@0.50.10) (2019-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.8...@ohif/extension-vtk@0.50.9) (2019-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.7...@ohif/extension-vtk@0.50.8) (2019-09-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.6...@ohif/extension-vtk@0.50.7) (2019-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.5...@ohif/extension-vtk@0.50.6) (2019-09-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.4...@ohif/extension-vtk@0.50.5) (2019-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
|
||||
|
||||
|
||||
|
||||
|
||||
- measurementsAPI issue caused by production build
|
||||
([#842](https://github.com/OHIF/Viewers/issues/842))
|
||||
([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
|
||||
|
||||
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.3...@ohif/extension-vtk@0.50.4) (2019-08-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.2...@ohif/extension-vtk@0.50.3) (2019-08-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.1...@ohif/extension-vtk@0.50.2) (2019-08-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update vtk and viewport dependencies ([98c40bc](https://github.com/OHIF/Viewers/commit/98c40bc))
|
||||
|
||||
|
||||
|
||||
|
||||
- Update vtk and viewport dependencies
|
||||
([98c40bc](https://github.com/OHIF/Viewers/commit/98c40bc))
|
||||
|
||||
## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.0-alpha.10...@ohif/extension-vtk@0.50.1) (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.1.4-alpha.9...@ohif/extension-vtk@0.50.0-alpha.10) (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.4-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.1.4-alpha.8...@ohif/extension-vtk@0.1.4-alpha.9) (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.1.4-alpha.8 (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.2.10",
|
||||
"version": "1.3.6",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -33,8 +33,9 @@
|
||||
"@ohif/i18n": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dcmjs": "^0.10.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -49,12 +50,12 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
"react-vtkjs-viewport": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.3.5",
|
||||
"@ohif/ui": "^1.1.8",
|
||||
"cornerstone-tools": "^4.12.0",
|
||||
"@ohif/core": "^2.6.5",
|
||||
"@ohif/ui": "^1.4.1",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"gh-pages": "^2.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
import { connect } from 'react-redux';
|
||||
import VTKViewport from './VTKViewport';
|
||||
|
||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
@@ -18,6 +18,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const pluginDetails = viewportLayout.vtk || {};
|
||||
|
||||
return {
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
layout: state.viewports.layout,
|
||||
isActive,
|
||||
...pluginDetails,
|
||||
@@ -75,6 +76,6 @@ const ConnectedVTKViewport = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(View2D);
|
||||
)(VTKViewport);
|
||||
|
||||
export default ConnectedVTKViewport;
|
||||
@@ -1,46 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import LoadingIndicator from './LoadingIndicator.js';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
|
||||
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
|
||||
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||
|
||||
const segmentationModule = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
const SOP_CLASSES = {
|
||||
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
};
|
||||
|
||||
const specialCaseHandlers = {};
|
||||
specialCaseHandlers[
|
||||
SOP_CLASSES.SEGMENTATION_STORAGE
|
||||
] = handleSegmentationStorage;
|
||||
|
||||
// TODO: Figure out where we plan to put this long term
|
||||
const volumeCache = {};
|
||||
const labelmapCache = {};
|
||||
|
||||
/**
|
||||
* Create a labelmap image with the same dimensions as our background volume.
|
||||
*
|
||||
* @param backgroundImageData vtkImageData
|
||||
*/
|
||||
/* TODO: Not currently used until we have drawing tools in vtkjs.
|
||||
function createLabelMapImageData(backgroundImageData) {
|
||||
// TODO => Need to do something like this if we start drawing a new segmentation
|
||||
// On a vtkjs viewport.
|
||||
|
||||
const labelMapData = vtkImageData.newInstance(
|
||||
backgroundImageData.get('spacing', 'origin', 'direction')
|
||||
);
|
||||
@@ -55,7 +43,7 @@ function createLabelMapImageData(backgroundImageData) {
|
||||
labelMapData.getPointData().setScalars(dataArray);
|
||||
|
||||
return labelMapData;
|
||||
}
|
||||
} */
|
||||
|
||||
class OHIFVTKViewport extends Component {
|
||||
state = {
|
||||
@@ -68,15 +56,20 @@ class OHIFVTKViewport extends Component {
|
||||
viewportData: PropTypes.shape({
|
||||
studies: PropTypes.array,
|
||||
displaySet: PropTypes.shape({
|
||||
studyInstanceUid: PropTypes.string,
|
||||
displaySetInstanceUid: PropTypes.string,
|
||||
sopClassUids: PropTypes.arrayOf(PropTypes.string),
|
||||
sopInstanceUid: PropTypes.string,
|
||||
StudyInstanceUID: PropTypes.string,
|
||||
displaySetInstanceUID: PropTypes.string,
|
||||
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||
SOPInstanceUID: PropTypes.string,
|
||||
frameIndex: PropTypes.number,
|
||||
}),
|
||||
}),
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => {},
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@@ -92,18 +85,18 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
static getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) {
|
||||
// Create shortcut to displaySet
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
// Get stack from Stack Manager
|
||||
@@ -114,17 +107,14 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
if (frameIndex !== undefined) {
|
||||
stack.currentImageIdIndex = frameIndex;
|
||||
} else if (sopInstanceUid) {
|
||||
} else if (SOPInstanceUID) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
'sopCommonModule',
|
||||
const imageIdSOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === sopInstanceUid;
|
||||
return imageIdSOPInstanceUID === SOPInstanceUID;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
@@ -139,48 +129,73 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
let imageDataObject;
|
||||
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||
let labelmapDataObject;
|
||||
let labelmapColorLUT;
|
||||
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
throw new Error('Not yet implemented');
|
||||
/*
|
||||
const data = handleSegmentationStorage(
|
||||
stack.imageIds,
|
||||
displaySetInstanceUid
|
||||
const firstImageId = stack.imageIds[0];
|
||||
const { state } = segmentationModule;
|
||||
const brushStackState = state.series[firstImageId];
|
||||
|
||||
if (brushStackState) {
|
||||
const { activeLabelmapIndex } = brushStackState;
|
||||
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
||||
|
||||
const vtkLabelmapID = `${firstImageId}_${activeLabelmapIndex}`;
|
||||
|
||||
if (labelmapCache[vtkLabelmapID]) {
|
||||
labelmapDataObject = labelmapCache[vtkLabelmapID];
|
||||
} else {
|
||||
// TODO -> We need an imageId based getter in cornerstoneTools
|
||||
const labelmapBuffer = labelmap3D.buffer;
|
||||
|
||||
// Create VTK Image Data with buffer as input
|
||||
labelmapDataObject = vtkImageData.newInstance();
|
||||
|
||||
const dataArray = vtkDataArray.newInstance({
|
||||
numberOfComponents: 1, // labelmap with single component
|
||||
values: new Uint16Array(labelmapBuffer),
|
||||
});
|
||||
|
||||
labelmapDataObject.getPointData().setScalars(dataArray);
|
||||
labelmapDataObject.setDimensions(...imageDataObject.dimensions);
|
||||
labelmapDataObject.setSpacing(
|
||||
...imageDataObject.vtkImageData.getSpacing()
|
||||
);
|
||||
labelmapDataObject.setOrigin(
|
||||
...imageDataObject.vtkImageData.getOrigin()
|
||||
);
|
||||
labelmapDataObject.setDirection(
|
||||
...imageDataObject.vtkImageData.getDirection()
|
||||
);
|
||||
|
||||
imageDataObject = data.referenceDataObject;
|
||||
labelmapDataObject = data.labelmapDataObject;
|
||||
// Cache the labelmap volume.
|
||||
labelmapCache[vtkLabelmapID] = labelmapDataObject;
|
||||
}
|
||||
|
||||
return loadImageData(imageDataObject).then(() => {
|
||||
return {
|
||||
data: imageDataObject.vtkImageData,
|
||||
labelmap: labelmapDataObject,
|
||||
};
|
||||
});
|
||||
*/
|
||||
default:
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
|
||||
|
||||
return imageDataObject;
|
||||
labelmapColorLUT = state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
}
|
||||
|
||||
return {
|
||||
imageDataObject,
|
||||
labelmapDataObject,
|
||||
labelmapColorLUT,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -189,25 +204,31 @@ class OHIFVTKViewport extends Component {
|
||||
* @param {object} imageDataObject
|
||||
* @param {object} imageDataObject.vtkImageData
|
||||
* @param {object} imageDataObject.imageMetaData0
|
||||
* @param {number} [imageDataObject.imageMetaData0.windowWidth] - The volume's initial windowWidth
|
||||
* @param {number} [imageDataObject.imageMetaData0.windowCenter] - The volume's initial windowCenter
|
||||
* @param {string} imageDataObject.imageMetaData0.modality - CT, MR, PT, etc
|
||||
* @param {string} displaySetInstanceUid
|
||||
* @param {number} [imageDataObject.imageMetaData0.WindowWidth] - The volume's initial WindowWidth
|
||||
* @param {number} [imageDataObject.imageMetaData0.WindowCenter] - The volume's initial WindowCenter
|
||||
* @param {string} imageDataObject.imageMetaData0.Modality - CT, MR, PT, etc
|
||||
* @param {string} displaySetInstanceUID
|
||||
* @returns vtkVolumeActor
|
||||
* @memberof OHIFVTKViewport
|
||||
*/
|
||||
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
|
||||
if (volumeCache[displaySetInstanceUid]) {
|
||||
return volumeCache[displaySetInstanceUid];
|
||||
getOrCreateVolume(imageDataObject, displaySetInstanceUID) {
|
||||
if (volumeCache[displaySetInstanceUID]) {
|
||||
return volumeCache[displaySetInstanceUID];
|
||||
}
|
||||
|
||||
const { vtkImageData, imageMetaData0 } = imageDataObject;
|
||||
const { windowWidth, windowCenter, modality } = imageMetaData0;
|
||||
// TODO -> Should update react-vtkjs-viewport and react-cornerstone-viewports
|
||||
// internals to use naturalized DICOM JSON names.
|
||||
const {
|
||||
windowWidth: WindowWidth,
|
||||
windowCenter: WindowCenter,
|
||||
modality: Modality,
|
||||
} = imageMetaData0;
|
||||
|
||||
const { lower, upper } = _getRangeFromWindowLevels(
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
modality
|
||||
WindowWidth,
|
||||
WindowCenter,
|
||||
Modality
|
||||
);
|
||||
const volumeActor = vtkVolume.newInstance();
|
||||
const volumeMapper = vtkVolumeMapper.newInstance();
|
||||
@@ -231,7 +252,7 @@ class OHIFVTKViewport extends Component {
|
||||
// TODO: maybe we should auto adjust samples to 1000.
|
||||
volumeMapper.setMaximumSamplesPerRay(4000);
|
||||
|
||||
volumeCache[displaySetInstanceUid] = volumeActor;
|
||||
volumeCache[displaySetInstanceUID] = volumeActor;
|
||||
|
||||
return volumeActor;
|
||||
}
|
||||
@@ -239,53 +260,61 @@ class OHIFVTKViewport extends Component {
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUIDs,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (sopClassUids.length > 1) {
|
||||
if (sopClassUIDs.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids[0];
|
||||
const imageDataObject = this.getViewportData(
|
||||
const study = studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const dataDetails = {
|
||||
studyDate: study.studyDate,
|
||||
studyTime: study.studyTime,
|
||||
studyDescription: study.studyDescription,
|
||||
patientName: study.patientName,
|
||||
patientId: study.patientId,
|
||||
seriesNumber: String(displaySet.seriesNumber),
|
||||
seriesDescription: displaySet.seriesDescription,
|
||||
};
|
||||
|
||||
const {
|
||||
imageDataObject,
|
||||
labelmapDataObject,
|
||||
labelmapColorLUT,
|
||||
} = this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
this.imageDataObject = imageDataObject;
|
||||
|
||||
// TODO: Temporarily disabling this since it is not yet
|
||||
// being used and hurts performance significantly.
|
||||
/*if (!labelmap) {
|
||||
/* TODO: Not currently used until we have drawing tools in vtkjs.
|
||||
if (!labelmap) {
|
||||
labelmap = createLabelMapImageData(data);
|
||||
}*/
|
||||
} */
|
||||
|
||||
const volumeActor = this.getOrCreateVolume(
|
||||
imageDataObject,
|
||||
displaySetInstanceUid
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
this.setState({
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
paintFilterLabelMapImageData: null, // TODO
|
||||
percentComplete: 0,
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
paintFilterLabelMapImageData: null, // TODO
|
||||
percentComplete: 0,
|
||||
dataDetails,
|
||||
},
|
||||
() => {
|
||||
this.loadProgressively(imageDataObject);
|
||||
@@ -297,6 +326,9 @@ class OHIFVTKViewport extends Component {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
paintFilterLabelMapImageData: labelmapDataObject,
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
labelmapColorLUT,
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
@@ -308,13 +340,13 @@ class OHIFVTKViewport extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUid !==
|
||||
prevDisplaySet.displaySetInstanceUid ||
|
||||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
@@ -326,7 +358,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
const { isLoading, insertPixelDataPromises } = imageDataObject;
|
||||
|
||||
const numberOfFrames = insertPixelDataPromises.length;
|
||||
const NumberOfFrames = insertPixelDataPromises.length;
|
||||
|
||||
if (!isLoading) {
|
||||
this.setState({ isLoaded: true });
|
||||
@@ -336,7 +368,7 @@ class OHIFVTKViewport extends Component {
|
||||
insertPixelDataPromises.forEach(promise => {
|
||||
promise.then(numberProcessed => {
|
||||
const percentComplete = Math.floor(
|
||||
(numberProcessed * 100) / numberOfFrames
|
||||
(numberProcessed * 100) / NumberOfFrames
|
||||
);
|
||||
|
||||
if (percentComplete !== this.state.percentComplete) {
|
||||
@@ -348,12 +380,15 @@ class OHIFVTKViewport extends Component {
|
||||
});
|
||||
|
||||
Promise.all(insertPixelDataPromises).then(() => {
|
||||
this.setState({ isLoaded: true });
|
||||
this.setState({
|
||||
isLoaded: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let childrenWithProps = null;
|
||||
const { configuration } = segmentationModule;
|
||||
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
@@ -370,6 +405,10 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
const visible = configuration.renderFill || configuration.renderOutline;
|
||||
const opacity = configuration.fillAlpha;
|
||||
const outlineThickness = configuration.outlineThickness;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={style}>
|
||||
@@ -386,6 +425,15 @@ class OHIFVTKViewport extends Component {
|
||||
this.state.paintFilterBackgroundImageData
|
||||
}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
dataDetails={this.state.dataDetails}
|
||||
labelmapRenderingOptions={{
|
||||
colorLUT: this.state.labelmapColorLUT,
|
||||
globalOpacity: opacity,
|
||||
visible,
|
||||
outlineThickness,
|
||||
renderOutline: true,
|
||||
}}
|
||||
onScroll={this.props.onScroll}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -403,21 +451,21 @@ class OHIFVTKViewport extends Component {
|
||||
* @private
|
||||
* @param {number} [width] - the width of our window
|
||||
* @param {number} [center] - the center of our window
|
||||
* @param {string} [modality] - 'PT', 'CT', etc.
|
||||
* @param {string} [Modality] - 'PT', 'CT', etc.
|
||||
* @returns { lower, upper } - range
|
||||
*/
|
||||
function _getRangeFromWindowLevels(width, center, modality = undefined) {
|
||||
function _getRangeFromWindowLevels(width, center, Modality = undefined) {
|
||||
// For PET just set the range to 0-5 SUV
|
||||
if (Modality === 'PT') {
|
||||
return { lower: 0, upper: 5 };
|
||||
}
|
||||
|
||||
const levelsAreNotNumbers = isNaN(center) || isNaN(width);
|
||||
|
||||
if (levelsAreNotNumbers) {
|
||||
return { lower: 0, upper: 512 };
|
||||
}
|
||||
|
||||
// For PET just set the range to 0-5 SUV
|
||||
if (modality === 'PT') {
|
||||
return { lower: 0, upper: 5 };
|
||||
}
|
||||
|
||||
return {
|
||||
lower: center - width / 2.0,
|
||||
upper: center + width / 2.0,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.vtk-viewport-handler svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './VTKViewport.css';
|
||||
|
||||
const VTKViewport = props => {
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
const setViewportActiveHandler = useCallback(() => {
|
||||
const { setViewportActive, viewportIndex, activeViewportIndex } = props;
|
||||
|
||||
if (viewportIndex !== activeViewportIndex) {
|
||||
// set in Connected
|
||||
setViewportActive();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleScrollEvent = evt => {
|
||||
const vtkViewportApiReference = props.onScroll(props.viewportIndex) || {};
|
||||
const viewportUID = vtkViewportApiReference.uid;
|
||||
const viewportWasScrolled = viewportUID === evt.detail.uid;
|
||||
|
||||
if (viewportWasScrolled) {
|
||||
setViewportActiveHandler();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('vtkscrollevent', handleScrollEvent);
|
||||
return () =>
|
||||
window.removeEventListener('vtkscrollevent', handleScrollEvent);
|
||||
}, [props, props.onScroll, props.viewportIndex, setViewportActiveHandler]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="vtk-viewport-handler"
|
||||
style={style}
|
||||
onClick={setViewportActiveHandler}
|
||||
>
|
||||
<View2D {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VTKViewport.propTypes = {
|
||||
setViewportActive: PropTypes.func.isRequired,
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
/* Receives viewportIndex */
|
||||
onScroll: PropTypes.func,
|
||||
};
|
||||
|
||||
VTKViewport.defaultProps = {
|
||||
onScroll: () => {},
|
||||
};
|
||||
|
||||
export default VTKViewport;
|
||||
@@ -70,12 +70,9 @@ const commandsModule = ({ commandsManager }) => {
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
const Modality = cornerstone.metaData.get('Modality', imageId);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
if (Modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
@@ -103,6 +100,9 @@ const commandsModule = ({ commandsManager }) => {
|
||||
}
|
||||
|
||||
const actions = {
|
||||
getVtkApis: ({ index }) => {
|
||||
return apis[index];
|
||||
},
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
@@ -266,11 +266,12 @@ const commandsModule = ({ commandsManager }) => {
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const uid = api.uid;
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
configuration: { apis, apiIndex, uid },
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -296,55 +297,45 @@ const commandsModule = ({ commandsManager }) => {
|
||||
},
|
||||
enableRotateTool: {
|
||||
commandFn: actions.enableRotateTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
},
|
||||
setBlendModeToMaximumIntensity: {
|
||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToMinimumIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToAverageIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||
},
|
||||
setSlabThickness: {
|
||||
// TODO: How do we pass in a function argument?
|
||||
commandFn: actions.setSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
increaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: 3,
|
||||
},
|
||||
},
|
||||
decreaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: -3,
|
||||
},
|
||||
@@ -355,6 +346,10 @@ const commandsModule = ({ commandsManager }) => {
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
getVtkApiForViewportIndex: {
|
||||
commandFn: actions.getVtkApis,
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import * as dcmjs from "dcmjs";
|
||||
|
||||
import OHIF from "@ohif/core";
|
||||
import { api } from "dicomweb-client";
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
return study.displaySets.filter(set => {
|
||||
return set.seriesInstanceUid === seriesInstanceUid;
|
||||
});
|
||||
}
|
||||
|
||||
function getCornerstoneStack(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
// Get stack from Stack Manager
|
||||
const stack = StackManager.findOrCreateStack(study, displaySet);
|
||||
|
||||
// Clone the stack here so we don't mutate it later
|
||||
const stackClone = Object.assign({}, stack);
|
||||
stackClone.currentImageIdIndex = 0;
|
||||
|
||||
return stackClone;
|
||||
}
|
||||
|
||||
function retrieveDicomData(
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
wadoRoot
|
||||
) {
|
||||
const config = {
|
||||
url: wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader()
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID
|
||||
};
|
||||
|
||||
return dicomWeb.retrieveInstance(options);
|
||||
}
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
// TODO: This is terrible but we need to use WADO-RS or we can't retrieve the SEG
|
||||
// from google cloud
|
||||
const wadoRoot = displaySet.images[0].getData().wadoRoot;
|
||||
|
||||
const StudyInstanceUID = displaySet.images[0].getStudyInstanceUID();
|
||||
const SeriesInstanceUID = displaySet.images[0].getSeriesInstanceUID();
|
||||
const SOPInstanceUID = displaySet.images[0].getSOPInstanceUID();
|
||||
|
||||
const arrayBuffer = await retrieveDicomData(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
wadoRoot
|
||||
);
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
const segments = dcmjs.adapters.VTKjs.Segmentation.generateSegments(dataset);
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const displaySets = getDisplaySetsBySeries(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid
|
||||
);
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
"More than one display set with the same seriesInstanceUid. This is not supported yet..."
|
||||
);
|
||||
}
|
||||
|
||||
const referenceDisplaySet = displaySets[0];
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.getImageId());
|
||||
|
||||
if (!results) {
|
||||
throw new Error("Fractional segmentations are not supported");
|
||||
}
|
||||
|
||||
const cachedStack = StackManager.findOrCreateStack(
|
||||
study,
|
||||
referenceDisplaySet
|
||||
);
|
||||
const stack = Object.assign({}, cachedStack);
|
||||
stack.currentImageIdIndex = 0;
|
||||
|
||||
return {
|
||||
referenceDataObject,
|
||||
labelmapDataObject
|
||||
};
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack
|
||||
};
|
||||
}
|
||||
|
||||
export default handleSegmentationStorage;
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import withCommandsManager from './withCommandsManager.js';
|
||||
// This feels weird
|
||||
// import loadLocales from './loadLocales';
|
||||
|
||||
@@ -14,8 +15,8 @@ const vtkExtension = {
|
||||
*/
|
||||
id: 'vtk',
|
||||
|
||||
getViewportModule() {
|
||||
return OHIFVTKViewport;
|
||||
getViewportModule({ commandsManager }) {
|
||||
return withCommandsManager(OHIFVTKViewport, commandsManager);
|
||||
},
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
|
||||
@@ -13,21 +13,21 @@ const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportI
|
||||
return false;
|
||||
};
|
||||
|
||||
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
|
||||
activeViewportIndex
|
||||
];
|
||||
|
||||
const studies = studyMetadataManager.all();
|
||||
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUID === studyInstanceUid
|
||||
study => study.studyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const displaySet = study._displaySets.find(set => set.displaySetInstanceUid === displaySetInstanceUid);
|
||||
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
|
||||
|
||||
if (!displaySet) {
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function withCommandsManager(Component, commandsManager = {}) {
|
||||
return class WithCommandsManager extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Component
|
||||
{...this.props}
|
||||
onScroll={viewportIndex =>
|
||||
commandsManager.runCommand('getVtkApiForViewportIndex', {
|
||||
index: viewportIndex,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
+2
-1
@@ -10,7 +10,8 @@ module.exports = {
|
||||
// https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig
|
||||
projects: [
|
||||
// "<rootDir>/extensions/*/jest.config.js",
|
||||
"<rootDir>/platform/*/jest.config.js"
|
||||
"<rootDir>/platform/*/jest.config.js",
|
||||
"<rootDir>/extensions/*/jest.config.js"
|
||||
],
|
||||
coverageDirectory: "<rootDir>/coverage/"
|
||||
};
|
||||
+3
-1
@@ -13,11 +13,13 @@
|
||||
"scripts": {
|
||||
"cm": "npx git-cz",
|
||||
"build": "lerna run build:viewer --stream",
|
||||
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
|
||||
"build:ci": "lerna run build:viewer:ci --stream",
|
||||
"build:demo": "lerna run build:viewer:demo --stream",
|
||||
"build:package": "lerna run build:viewer:package --stream",
|
||||
"build:package-all": "lerna run build:package --parallel --stream",
|
||||
"dev": "lerna run dev:viewer --stream",
|
||||
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
|
||||
"dev:project": ".scripts/dev.sh",
|
||||
"dev:orthanc": "lerna run dev:orthanc --stream",
|
||||
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
||||
@@ -59,6 +61,7 @@
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"cypress-file-upload": "^3.5.3",
|
||||
"dotenv": "^8.1.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
@@ -72,7 +75,6 @@
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.5.0",
|
||||
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
||||
"favicons-webpack-plugin": "^1.0.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^24.8.0",
|
||||
|
||||
+248
-178
@@ -3,6 +3,176 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.6.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.4...@ohif/core@2.6.5) (2020-04-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* segmentation not loading ([#1566](https://github.com/OHIF/Viewers/issues/1566)) ([4a7ce1c](https://github.com/OHIF/Viewers/commit/4a7ce1c09324d74c61048393e3a2427757e4001a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.3...@ohif/core@2.6.4) (2020-03-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add support for single entries in SequenceOfUltrasoundRegions. M… ([#1559](https://github.com/OHIF/Viewers/issues/1559)) ([c1a0d3c](https://github.com/OHIF/Viewers/commit/c1a0d3c662d143b62dfbf1c01f6ce394af3756ca))
|
||||
* disable autoFreeze of immer, even in dev mode ([#1560](https://github.com/OHIF/Viewers/issues/1560)) ([d604eba](https://github.com/OHIF/Viewers/commit/d604ebaffd93f688eadd0081e402f27074dd226b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.2...@ohif/core@2.6.3) (2020-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ensure we take into account pixel spacing fields properly ([#1555](https://github.com/OHIF/Viewers/issues/1555)) ([77ab0ad](https://github.com/OHIF/Viewers/commit/77ab0ad9a14a135b5560741fc1600704df08c141))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.1...@ohif/core@2.6.2) (2020-03-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* OverlayPlane module usage for ArrayBuffer, BulkDataURI, and InlineBinary cases, as well as PaletteColor LUTs for ArrayBuffer (i.e. local drag/drop) case ([#1546](https://github.com/OHIF/Viewers/issues/1546)) ([404d52f](https://github.com/OHIF/Viewers/commit/404d52fe5c0442dd13e4d407bb0687d72fa5f32c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.0...@ohif/core@2.6.1) (2020-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* avoid-wasteful-renders ([#1544](https://github.com/OHIF/Viewers/issues/1544)) ([e41d339](https://github.com/OHIF/Viewers/commit/e41d339f5faef6b93700bc860f37f29f32ad5ed6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.3...@ohif/core@2.6.0) (2020-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Segmentations Settings UI - Phase 1 [#1391](https://github.com/OHIF/Viewers/issues/1391) ([#1392](https://github.com/OHIF/Viewers/issues/1392)) ([e8842cf](https://github.com/OHIF/Viewers/commit/e8842cf8aebde98db7fc123e4867c8288552331f)), closes [#1423](https://github.com/OHIF/Viewers/issues/1423)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.5.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.2...@ohif/core@2.5.3) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.1...@ohif/core@2.5.2) (2020-03-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.0...@ohif/core@2.5.1) (2020-02-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 💡 Update log to warn ([#1454](https://github.com/OHIF/Viewers/issues/1454)) ([509a7b7](https://github.com/OHIF/Viewers/commit/509a7b7e57834a0653add98cc320b2a5dbefd51d)), closes [#1451](https://github.com/OHIF/Viewers/issues/1451)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.1...@ohif/core@2.5.0) (2020-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.0...@ohif/core@2.4.1) (2020-02-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.9...@ohif/core@2.4.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.8...@ohif/core@2.3.9) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.7...@ohif/core@2.3.8) (2020-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove trash data from redux storage after updates ([#1358](https://github.com/OHIF/Viewers/issues/1358)) ([7b2d44f](https://github.com/OHIF/Viewers/commit/7b2d44f2c18241ea521b8d3652aee32e36eaddb8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.6...@ohif/core@2.3.7) (2020-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.5...@ohif/core@2.3.6) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.4...@ohif/core@2.3.5) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
@@ -206,339 +376,239 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
|
||||
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.8.0...@ohif/core@1.9.0) (2019-11-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||
|
||||
|
||||
|
||||
|
||||
- expose UiNotifications service
|
||||
([#1172](https://github.com/OHIF/Viewers/issues/1172))
|
||||
([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||
|
||||
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.1...@ohif/core@1.8.0) (2019-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Update hotkeys and user preferences modal
|
||||
([#1135](https://github.com/OHIF/Viewers/issues/1135))
|
||||
([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)),
|
||||
closes [#923](https://github.com/OHIF/Viewers/issues/923)
|
||||
|
||||
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.0...@ohif/core@1.7.1) (2019-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
|
||||
|
||||
|
||||
|
||||
|
||||
- Add a fallback metadata provider which pulls metadata from WADO-…
|
||||
([#1158](https://github.com/OHIF/Viewers/issues/1158))
|
||||
([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
|
||||
|
||||
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.2...@ohif/core@1.7.0) (2019-11-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Filter by url query param for seriesInstnaceUID ([#1117](https://github.com/OHIF/Viewers/issues/1117)) ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Filter by url query param for seriesInstnaceUID
|
||||
([#1117](https://github.com/OHIF/Viewers/issues/1117))
|
||||
([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)),
|
||||
closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
|
||||
|
||||
## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.1...@ohif/core@1.6.2) (2019-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study List before all series have finishe… ([#1090](https://github.com/OHIF/Viewers/issues/1090)) ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
|
||||
|
||||
|
||||
|
||||
|
||||
- [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study
|
||||
List before all series have finishe…
|
||||
([#1090](https://github.com/OHIF/Viewers/issues/1090))
|
||||
([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
|
||||
|
||||
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.0...@ohif/core@1.6.1) (2019-10-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* application crash if patientName is an object ([#1138](https://github.com/OHIF/Viewers/issues/1138)) ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
|
||||
|
||||
|
||||
|
||||
|
||||
- application crash if patientName is an object
|
||||
([#1138](https://github.com/OHIF/Viewers/issues/1138))
|
||||
([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
|
||||
|
||||
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.2...@ohif/core@1.6.0) (2019-10-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840)) ([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
|
||||
|
||||
|
||||
|
||||
|
||||
- Snapshot Download Tool ([#840](https://github.com/OHIF/Viewers/issues/840))
|
||||
([450e098](https://github.com/OHIF/Viewers/commit/450e0981a5ba054fcfcb85eeaeb18371af9088f8))
|
||||
|
||||
## [1.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.1...@ohif/core@1.5.2) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* set SR in ActiveViewport by clicking thumb ([#1091](https://github.com/OHIF/Viewers/issues/1091)) ([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79))
|
||||
|
||||
|
||||
|
||||
|
||||
- set SR in ActiveViewport by clicking thumb
|
||||
([#1091](https://github.com/OHIF/Viewers/issues/1091))
|
||||
([986b7ae](https://github.com/OHIF/Viewers/commit/986b7ae2bf4f7d27f326e62f93285ce20eaf0a79))
|
||||
|
||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.0...@ohif/core@1.5.1) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092))
|
||||
([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.4.0...@ohif/core@1.5.0) (2019-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List ([#1069](https://github.com/OHIF/Viewers/issues/1069)) ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List
|
||||
([#1069](https://github.com/OHIF/Viewers/issues/1069))
|
||||
([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
|
||||
|
||||
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.2...@ohif/core@1.4.0) (2019-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Only allow reconstruction of datasets that make sense
|
||||
([#1010](https://github.com/OHIF/Viewers/issues/1010))
|
||||
([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes
|
||||
[#561](https://github.com/OHIF/Viewers/issues/561)
|
||||
|
||||
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.1...@ohif/core@1.3.2) (2019-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Return display sets in StudyMetadata._createDisplaySetsForSeries ([#1042](https://github.com/OHIF/Viewers/issues/1042)) ([fc01532](https://github.com/OHIF/Viewers/commit/fc01532))
|
||||
|
||||
|
||||
|
||||
|
||||
- Return display sets in StudyMetadata.\_createDisplaySetsForSeries
|
||||
([#1042](https://github.com/OHIF/Viewers/issues/1042))
|
||||
([fc01532](https://github.com/OHIF/Viewers/commit/fc01532))
|
||||
|
||||
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.0...@ohif/core@1.3.1) (2019-10-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.2.0...@ohif/core@1.3.0) (2019-10-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Improve usability of Google Cloud adapter, including direct routes to studies ([#989](https://github.com/OHIF/Viewers/issues/989)) ([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c))
|
||||
|
||||
|
||||
|
||||
|
||||
- 🎸 Improve usability of Google Cloud adapter, including direct routes to
|
||||
studies ([#989](https://github.com/OHIF/Viewers/issues/989))
|
||||
([2bc361c](https://github.com/OHIF/Viewers/commit/2bc361c))
|
||||
|
||||
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.1.0...@ohif/core@1.2.0) (2019-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* OHIF-1002 Study lazy load should be true by default ([#1004](https://github.com/OHIF/Viewers/issues/1004)) ([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6))
|
||||
|
||||
- OHIF-1002 Study lazy load should be true by default
|
||||
([#1004](https://github.com/OHIF/Viewers/issues/1004))
|
||||
([66d8bc6](https://github.com/OHIF/Viewers/commit/66d8bc6))
|
||||
|
||||
### Features
|
||||
|
||||
* Allow a server requestOptions.auth to be a function that returns the Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928)) ([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b))
|
||||
|
||||
|
||||
|
||||
|
||||
- Allow a server requestOptions.auth to be a function that returns the
|
||||
Authorization header. ([#928](https://github.com/OHIF/Viewers/issues/928))
|
||||
([0064a4b](https://github.com/OHIF/Viewers/commit/0064a4b))
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.2...@ohif/core@1.1.0) (2019-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
|
||||
|
||||
|
||||
|
||||
|
||||
- Use QIDO + WADO to load series metadata individually rather than the entire
|
||||
study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953))
|
||||
([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.1...@ohif/core@1.0.2) (2019-10-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues ([#987](https://github.com/OHIF/Viewers/issues/987)) ([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
|
||||
|
||||
|
||||
|
||||
|
||||
- Temporarily sort SEG files to the end of the display set list as a workaround
|
||||
for several metadata issues
|
||||
([#987](https://github.com/OHIF/Viewers/issues/987))
|
||||
([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Check for Value in 00081155 sequence (Few patient protocol images doesn't have this value) and removed a duplicate declaration ([#921](https://github.com/OHIF/Viewers/issues/921)) ([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf))
|
||||
|
||||
|
||||
|
||||
|
||||
- Check for Value in 00081155 sequence (Few patient protocol images doesn't have
|
||||
this value) and removed a duplicate declaration
|
||||
([#921](https://github.com/OHIF/Viewers/issues/921))
|
||||
([d0ec9cf](https://github.com/OHIF/Viewers/commit/d0ec9cf))
|
||||
|
||||
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.10...@ohif/core@1.0.0) (2019-09-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support in local file and WADO-RS-only use cases ([#862](https://github.com/OHIF/Viewers/issues/862)) ([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes [#838](https://github.com/OHIF/Viewers/issues/838)
|
||||
|
||||
- 🐛 Add DicomLoaderService & FileLoaderService to fix SR, PDF, and SEG support
|
||||
in local file and WADO-RS-only use cases
|
||||
([#862](https://github.com/OHIF/Viewers/issues/862))
|
||||
([e7e1a8a](https://github.com/OHIF/Viewers/commit/e7e1a8a)), closes
|
||||
[#838](https://github.com/OHIF/Viewers/issues/838)
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* DICOM Seg
|
||||
|
||||
|
||||
|
||||
|
||||
- DICOM Seg
|
||||
|
||||
## [0.50.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.9...@ohif/core@0.50.10) (2019-09-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Avoid using variable name "module" ([#942](https://github.com/OHIF/Viewers/issues/942)) ([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes [#940](https://github.com/OHIF/Viewers/issues/940)
|
||||
|
||||
|
||||
|
||||
|
||||
- Avoid using variable name "module"
|
||||
([#942](https://github.com/OHIF/Viewers/issues/942))
|
||||
([72427fe](https://github.com/OHIF/Viewers/commit/72427fe)), closes
|
||||
[#940](https://github.com/OHIF/Viewers/issues/940)
|
||||
|
||||
## [0.50.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.8...@ohif/core@0.50.9) (2019-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump cornerstone-tools version in peerDeps ([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||
|
||||
|
||||
|
||||
|
||||
- bump cornerstone-tools version in peerDeps
|
||||
([4afc88c](https://github.com/OHIF/Viewers/commit/4afc88c))
|
||||
|
||||
## [0.50.8](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.7...@ohif/core@0.50.8) (2019-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.6...@ohif/core@0.50.7) (2019-09-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove requestOptions when key is not needed ([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d))
|
||||
|
||||
|
||||
|
||||
|
||||
- remove requestOptions when key is not needed
|
||||
([32bc47d](https://github.com/OHIF/Viewers/commit/32bc47d))
|
||||
|
||||
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.5...@ohif/core@0.50.6) (2019-09-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.4...@ohif/core@0.50.5) (2019-09-04)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.3...@ohif/core@0.50.4) (2019-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* measurementsAPI issue caused by production build ([#842](https://github.com/OHIF/Viewers/issues/842)) ([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
|
||||
|
||||
|
||||
|
||||
|
||||
- measurementsAPI issue caused by production build
|
||||
([#842](https://github.com/OHIF/Viewers/issues/842))
|
||||
([49d3439](https://github.com/OHIF/Viewers/commit/49d3439))
|
||||
|
||||
## [0.50.3](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.2...@ohif/core@0.50.3) (2019-08-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Studies:** Qidosupportsincludefield should be true by default ([#801](https://github.com/OHIF/Viewers/issues/801)) ([a88d865](https://github.com/OHIF/Viewers/commit/a88d865))
|
||||
|
||||
|
||||
|
||||
|
||||
- **Studies:** Qidosupportsincludefield should be true by default
|
||||
([#801](https://github.com/OHIF/Viewers/issues/801))
|
||||
([a88d865](https://github.com/OHIF/Viewers/commit/a88d865))
|
||||
|
||||
## [0.50.2](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.1...@ohif/core@0.50.2) (2019-08-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.1](https://github.com/OHIF/Viewers/compare/@ohif/core@0.50.0-alpha.10...@ohif/core@0.50.1) (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.50.0-alpha.10](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.9...@ohif/core@0.50.0-alpha.10) (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.11.1-alpha.9](https://github.com/OHIF/Viewers/compare/@ohif/core@0.11.1-alpha.8...@ohif/core@0.11.1-alpha.9) (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 0.11.1-alpha.8 (2019-08-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
|
||||
@@ -66,8 +66,8 @@ import { studies } from '@ohif/core';
|
||||
|
||||
const studiesMetadata = await studies.retrieveStudiesMetadata(
|
||||
server, // Object
|
||||
studyInstanceUids, // Array
|
||||
seriesInstanceUids // Array (optional)
|
||||
studyInstanceUIDs, // Array
|
||||
seriesInstanceUIDs // Array (optional)
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.3.5",
|
||||
"version": "2.6.5",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.12.0",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
@@ -41,6 +41,7 @@
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "^0.8.3",
|
||||
"dicomweb-client": "^0.5.2",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.merge": "^4.6.1",
|
||||
|
||||
@@ -31,6 +31,7 @@ const retrieveMeasurements = server => {
|
||||
|
||||
const serverUrl = server.wadoRoot;
|
||||
const studies = utils.studyMetadataManager.all();
|
||||
|
||||
const latestSeries = findMostRecentStructuredReport(studies);
|
||||
|
||||
if (!latestSeries) return Promise.resolve({});
|
||||
@@ -57,17 +58,17 @@ const storeMeasurements = async (measurementData, filter, server) => {
|
||||
const serverUrl = server.wadoRoot;
|
||||
const firstMeasurementKey = Object.keys(measurementData)[0];
|
||||
const firstMeasurement = measurementData[firstMeasurementKey][0];
|
||||
const studyInstanceUid =
|
||||
firstMeasurement && firstMeasurement.studyInstanceUid;
|
||||
const StudyInstanceUID =
|
||||
firstMeasurement && firstMeasurement.StudyInstanceUID;
|
||||
|
||||
try {
|
||||
await stowSRFromMeasurements(measurementData, serverUrl);
|
||||
if (studyInstanceUid) {
|
||||
studies.deleteStudyMetadataPromise(studyInstanceUid);
|
||||
if (StudyInstanceUID) {
|
||||
studies.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Measurements were saved with success',
|
||||
message: 'Measurements saved successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
|
||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||
|
||||
/**
|
||||
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
|
||||
@@ -13,6 +13,7 @@ import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBy
|
||||
*/
|
||||
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
// Get the dicom data as an Object
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
@@ -30,17 +31,18 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
measurementData[toolName] = [];
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
const instanceMetadata = findInstanceMetadataBySopInstanceUid(
|
||||
const instanceMetadata = findInstanceMetadataBySopInstanceUID(
|
||||
displaySets,
|
||||
measurement.sopInstanceUid
|
||||
);
|
||||
|
||||
const { _study: study, _series: series } = instanceMetadata;
|
||||
const { studyInstanceUid, patientId } = study;
|
||||
const { seriesInstanceUid } = series;
|
||||
const { StudyInstanceUID, PatientID } = study;
|
||||
const { SeriesInstanceUID } = series;
|
||||
const { sopInstanceUid, frameIndex } = measurement;
|
||||
const imagePath = getImagePath(
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
@@ -56,9 +58,10 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
const toolData = Object.assign({}, measurement, {
|
||||
imageId,
|
||||
imagePath,
|
||||
seriesInstanceUid,
|
||||
studyInstanceUid,
|
||||
patientId,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
PatientID,
|
||||
measurementNumber: ++measurementNumber,
|
||||
timepointId: currentTimepointId,
|
||||
toolType: toolName,
|
||||
@@ -75,19 +78,19 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
/**
|
||||
* Function to create imagePath with all imageData related
|
||||
*
|
||||
* @param {string} studyInstanceUid
|
||||
* @param {string} seriesInstanceUid
|
||||
* @param {string} sopInstanceUid
|
||||
* @param {string} StudyInstanceUID
|
||||
* @param {string} SeriesInstanceUID
|
||||
* @param {string} SOPInstanceUID
|
||||
* @param {string} frameIndex
|
||||
* @returns
|
||||
*/
|
||||
const getImagePath = (
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join(
|
||||
return [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, frameIndex].join(
|
||||
'_'
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
* Should Find the requested instance metadata into the displaySets and return
|
||||
*
|
||||
* @param {Array} displaySets - List of displaySets
|
||||
* @param {string} sopInstanceUid - sopInstanceUID to look for
|
||||
* @param {string} SOPInstanceUID - sopInstanceUID to look for
|
||||
* @returns {Object} - instance metadata found
|
||||
*/
|
||||
const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
|
||||
const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
|
||||
let instanceFound;
|
||||
|
||||
displaySets.find(displaySet => {
|
||||
if (!displaySet.images) return false;
|
||||
|
||||
instanceFound = displaySet.images.find(
|
||||
instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid
|
||||
instanceMetadata =>
|
||||
instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
|
||||
);
|
||||
|
||||
return !!instanceFound;
|
||||
@@ -21,4 +22,4 @@ const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
|
||||
return instanceFound;
|
||||
};
|
||||
|
||||
export default findInstanceMetadataBySopInstanceUid;
|
||||
export default findInstanceMetadataBySopInstanceUID;
|
||||
@@ -37,9 +37,9 @@ const isStructuredReportSeries = series => {
|
||||
];
|
||||
|
||||
const firstInstance = series.getFirstInstance();
|
||||
const sopClassUid = firstInstance._instance.sopClassUid;
|
||||
const SOPClassUID = firstInstance.getData().metadata.SOPClassUID;
|
||||
|
||||
return supportedSopClassUIDs.includes(sopClassUid);
|
||||
return supportedSopClassUIDs.includes(SOPClassUID);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,9 +51,9 @@ const isStructuredReportSeries = series => {
|
||||
*/
|
||||
const compareSeriesDate = (series1, series2) => {
|
||||
return (
|
||||
series1._data.seriesDate > series2._data.seriesDate ||
|
||||
(series1._data.seriesDate === series2._data.seriesDate &&
|
||||
series1._data.seriesTime > series2._data.seriesTime)
|
||||
series1._data.SeriesDate > series2._data.SeriesDate ||
|
||||
(series1._data.SeriesDate === series2._data.SeriesDate &&
|
||||
series1._data.SeriesTime > series2._data.SeriesTime)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
export default function getModalities(modality, modalitiesInStudy) {
|
||||
if (!modality && !modalitiesInStudy) {
|
||||
export default function getModalities(Modality, ModalitiesInStudy) {
|
||||
if (!Modality && !ModalitiesInStudy) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const modalities = modality || {
|
||||
const modalities = Modality || {
|
||||
vr: 'CS',
|
||||
Value: [],
|
||||
};
|
||||
|
||||
if (modalitiesInStudy) {
|
||||
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
|
||||
for (let i = 0; i < modalitiesInStudy.Value.length; i++) {
|
||||
const value = modalitiesInStudy.Value[i];
|
||||
if (ModalitiesInStudy) {
|
||||
if (modalities.vr && modalities.vr === ModalitiesInStudy.vr) {
|
||||
for (let i = 0; i < ModalitiesInStudy.Value.length; i++) {
|
||||
const value = ModalitiesInStudy.Value[i];
|
||||
if (modalities.Value.indexOf(value) === -1) {
|
||||
modalities.Value.push(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return modalitiesInStudy;
|
||||
return ModalitiesInStudy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
import getModalities from './getModalities';
|
||||
|
||||
describe('getModalities', () => {
|
||||
test('should return an empty object when modality and modalitiesInStudy are not present', () => {
|
||||
const modality = null;
|
||||
const modalitiesInStudy = null;
|
||||
test('should return an empty object when Modality and ModalitiesInStudy are not present', () => {
|
||||
const Modality = null;
|
||||
const ModalitiesInStudy = null;
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual({});
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual({});
|
||||
});
|
||||
|
||||
test('should return an empty object when modality and modalitiesInStudy are not present', () => {
|
||||
const modality = null;
|
||||
const modalitiesInStudy = null;
|
||||
test('should return an empty object when Modality and ModalitiesInStudy are not present', () => {
|
||||
const Modality = null;
|
||||
const ModalitiesInStudy = null;
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual({});
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual({});
|
||||
});
|
||||
|
||||
test('should return modalities in Study when modality is not defined', () => {
|
||||
const modality = null;
|
||||
test('should return modalities in Study when Modality is not defined', () => {
|
||||
const Modality = null;
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['MOCKED_VALUE'],
|
||||
vr: 'MOCKED_VALUE',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(
|
||||
modalitiesInStudy
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(
|
||||
ModalitiesInStudy
|
||||
);
|
||||
});
|
||||
|
||||
test('should return only the modalitues that exists in modalitiesInStudy', () => {
|
||||
const modality = {
|
||||
test('should return only the modalitues that exists in ModalitiesInStudy', () => {
|
||||
const Modality = {
|
||||
Value: ['DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['DESIRED_VALUE', 'NOT_DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(modality);
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality);
|
||||
});
|
||||
|
||||
test('should return the seek modality when the desired modality does not exist in modalitiesInStudy', () => {
|
||||
const modality = {
|
||||
test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy', () => {
|
||||
const Modality = {
|
||||
Value: ['DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['NOT_DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(modality);
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality);
|
||||
});
|
||||
|
||||
test('should return the seek modality when the desired modality does not exist in modalitiesInStudy VR', () => {
|
||||
const modality = {
|
||||
test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy VR', () => {
|
||||
const Modality = {
|
||||
Value: ['DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['NOT_DESIRED_VALUE'],
|
||||
vr: 'ANOTHER_VR',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(
|
||||
modalitiesInStudy
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(
|
||||
ModalitiesInStudy
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
};
|
||||
@@ -161,7 +161,7 @@ export class CommandsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const { commandFn, storeContexts } = definition;
|
||||
const { commandFn, storeContexts = [] } = definition;
|
||||
const definitionOptions = definition.options;
|
||||
|
||||
let commandParams = {};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import hotkeys from './hotkeys';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ import log from './../log.js';
|
||||
*/
|
||||
|
||||
export class HotkeysManager {
|
||||
constructor(commandsManager) {
|
||||
constructor(commandsManager, servicesManager) {
|
||||
this.hotkeyDefinitions = {};
|
||||
this.hotkeyDefaults = [];
|
||||
this.isEnabled = true;
|
||||
@@ -22,9 +22,19 @@ export class HotkeysManager {
|
||||
);
|
||||
}
|
||||
|
||||
this._servicesManager = servicesManager;
|
||||
this._commandsManager = commandsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes Mousetrap.js's `.record` method, added by the record plugin.
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
record(event) {
|
||||
return hotkeys.record(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all hotkeys. Hotkeys added while disabled will not listen for
|
||||
* input.
|
||||
@@ -45,38 +55,57 @@ export class HotkeysManager {
|
||||
/**
|
||||
* Registers a list of hotkeydefinitions.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
setHotkeys(hotkeyDefinitions = []) {
|
||||
try {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
} catch (error) {
|
||||
const { UINotificationService } = this._servicesManager.services;
|
||||
UINotificationService.show({
|
||||
title: 'Hotkeys Manager',
|
||||
message: 'Erro while setting hotkeys',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default hotkey bindings. These
|
||||
* values are used in `this.restoreDefaultBindings`.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setDefaultHotKeys(hotkeyDefinitions) {
|
||||
setDefaultHotKeys(hotkeyDefinitions = []) {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take hotkey definitions that can be an array or object and make sure that it
|
||||
* returns an array of hotkeys
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
_getValidDefinitions(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
this.hotkeyDefaults = definitions;
|
||||
return definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* It parses given object containing hotkeyDefinition to array like.
|
||||
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
|
||||
* @returns {HotkeyDefinition[]}
|
||||
*/
|
||||
_parseToArrayLike(hotkeyDefinitionsObj) {
|
||||
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
|
||||
const copy = { ...hotkeyDefinitionsObj };
|
||||
return Object.entries(copy).map(entryValue =>
|
||||
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||
@@ -127,11 +156,13 @@ export class HotkeysManager {
|
||||
if (previouslyRegisteredDefinition) {
|
||||
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
|
||||
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
|
||||
log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
|
||||
}
|
||||
|
||||
// Set definition & bind
|
||||
this.hotkeyDefinitions[commandName] = { keys, label };
|
||||
this._bindHotkeys(commandName, keys);
|
||||
log.info(`Binding ${commandName} to ${keys}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,12 +198,11 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
const isKeyArray = keys instanceof Array;
|
||||
if (isKeyArray) {
|
||||
keys.forEach(key => this._bindHotkeys(commandName, key));
|
||||
return;
|
||||
}
|
||||
const combinedKeys = isKeyArray ? keys.join('+') : keys;
|
||||
|
||||
hotkeys.bind(keys, evt => {
|
||||
hotkeys.bind(combinedKeys, evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this._commandsManager.runCommand(commandName, { evt });
|
||||
});
|
||||
}
|
||||
@@ -193,7 +223,8 @@ export class HotkeysManager {
|
||||
|
||||
const isKeyArray = keys instanceof Array;
|
||||
if (isKeyArray) {
|
||||
keys.forEach(key => this._unbindHotkeys(commandName, key));
|
||||
const combinedKeys = keys.join('+');
|
||||
this._unbindHotkeys(commandName, combinedKeys);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import CommandsManager from './CommandsManager.js';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import hotkeys from './hotkeys';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
jest.mock('./CommandsManager.js');
|
||||
jest.mock('./hotkeys');
|
||||
jest.mock('./../utils/hotkeys');
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('HotkeysManager', () => {
|
||||
@@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
|
||||
});
|
||||
|
||||
describe('enable()', () => {
|
||||
beforeEach(() => hotkeys.unpause.mockClear());
|
||||
beforeEach(() => {
|
||||
hotkeys.unpause = jest.fn();
|
||||
hotkeys.unpause.mockClear();
|
||||
});
|
||||
|
||||
it('sets isEnabled property to true', () => {
|
||||
hotkeysManager.disable();
|
||||
@@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
|
||||
expectedHotkeyDefinition
|
||||
);
|
||||
});
|
||||
it('calls hotkeys.bind for all keys in array', () => {
|
||||
const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
|
||||
it('calls hotkeys.bind for the group of keys', () => {
|
||||
const definition = { commandName: 'dance', keys: ['shift', 'e'] };
|
||||
|
||||
hotkeysManager.registerHotkeys(definition);
|
||||
|
||||
expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
|
||||
definition.keys.forEach((key, i) =>
|
||||
expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
|
||||
);
|
||||
expect(hotkeys.bind.mock.calls.length).toBe(1);
|
||||
expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
|
||||
});
|
||||
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
|
||||
const firstDefinition = {
|
||||
commandName: 'dance',
|
||||
keys: ['h', 'e', 'l', 'o'],
|
||||
keys: ['alt', 'e'],
|
||||
};
|
||||
const secondDefinition = { commandName: 'dance', keys: 'a' };
|
||||
|
||||
@@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
|
||||
// Second call
|
||||
hotkeysManager.registerHotkeys(secondDefinition);
|
||||
|
||||
expect(hotkeys.unbind.mock.calls.length).toBe(
|
||||
firstDefinition.keys.length
|
||||
);
|
||||
firstDefinition.keys.forEach((key, i) =>
|
||||
expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
|
||||
);
|
||||
expect(hotkeys.unbind.mock.calls.length).toBe(1);
|
||||
expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import guid from '../utils/guid.js';
|
||||
import OHIFError from './OHIFError';
|
||||
import { Vector3 } from 'cornerstone-math';
|
||||
|
||||
const OBJECT = 'object';
|
||||
|
||||
@@ -63,6 +64,60 @@ class ImageSet {
|
||||
sortBy(sortingCallback) {
|
||||
return this.images.sort(sortingCallback);
|
||||
}
|
||||
|
||||
sortByImagePositionPatient() {
|
||||
const images = this.images;
|
||||
const referenceImagePositionPatient = _getImagePositionPatient(images[0]);
|
||||
|
||||
const refIppVec = new Vector3(
|
||||
referenceImagePositionPatient[0],
|
||||
referenceImagePositionPatient[1],
|
||||
referenceImagePositionPatient[2]
|
||||
);
|
||||
|
||||
const ImageOrientationPatient = _getImageOrientationPatient(images[0]);
|
||||
|
||||
const scanAxisNormal = new Vector3(
|
||||
ImageOrientationPatient[0],
|
||||
ImageOrientationPatient[1],
|
||||
ImageOrientationPatient[2]
|
||||
).cross(
|
||||
new Vector3(
|
||||
ImageOrientationPatient[3],
|
||||
ImageOrientationPatient[4],
|
||||
ImageOrientationPatient[5]
|
||||
)
|
||||
);
|
||||
|
||||
const distanceImagePairs = images.map(function(image) {
|
||||
const ippVec = new Vector3(..._getImagePositionPatient(image));
|
||||
const positionVector = refIppVec.clone().sub(ippVec);
|
||||
const distance = positionVector.dot(scanAxisNormal);
|
||||
|
||||
return {
|
||||
distance,
|
||||
image,
|
||||
};
|
||||
});
|
||||
|
||||
distanceImagePairs.sort(function(a, b) {
|
||||
return b.distance - a.distance;
|
||||
});
|
||||
|
||||
const sortedImages = distanceImagePairs.map(a => a.image);
|
||||
|
||||
images.sort(function(a, b) {
|
||||
return sortedImages.indexOf(a) - sortedImages.indexOf(b);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _getImagePositionPatient(image) {
|
||||
return image.getData().metadata.ImagePositionPatient;
|
||||
}
|
||||
|
||||
function _getImageOrientationPatient(image) {
|
||||
return image.getData().metadata.ImageOrientationPatient;
|
||||
}
|
||||
|
||||
export default ImageSet;
|
||||
@@ -1,412 +1,497 @@
|
||||
import { parsingUtils } from '../lib/parsingUtils';
|
||||
|
||||
const FUNCTION = 'function';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import queryString from 'query-string';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
|
||||
|
||||
class MetadataProvider {
|
||||
constructor() {
|
||||
// Define the main "metadataLookup" private property as an immutable property.
|
||||
Object.defineProperty(this, 'metadataLookup', {
|
||||
Object.defineProperty(this, 'studies', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: new Map(),
|
||||
});
|
||||
|
||||
// Local reference to provider function bound to current instance.
|
||||
Object.defineProperty(this, '_provider', {
|
||||
Object.defineProperty(this, 'imageIdToUIDs', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: null,
|
||||
writable: false,
|
||||
value: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cornerstone Metadata provider to store image meta data
|
||||
* Data from instances, series, and studies are associated with
|
||||
* imageIds to facilitate usage of this information by Cornerstone's Tools
|
||||
*
|
||||
* e.g. the imagePlane metadata object contains instance information about
|
||||
* row/column pixel spacing, patient position, and patient orientation. It
|
||||
* is used in CornerstoneTools to position reference lines and orientation markers.
|
||||
*
|
||||
* @param {String} imageId The Cornerstone ImageId
|
||||
* @param {Object} data An object containing instance, series, and study metadata
|
||||
*/
|
||||
addMetadata(imageId, data) {
|
||||
const instanceMetadata = data.instance;
|
||||
const seriesMetadata = data.series;
|
||||
const studyMetadata = data.study;
|
||||
const numImages = data.numImages;
|
||||
const metadata = {};
|
||||
async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) {
|
||||
let dicomJSONDataset;
|
||||
|
||||
metadata.frameNumber = data.frameNumber;
|
||||
// If Arraybuffer, parse to DICOMJSON before naturalizing.
|
||||
if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) {
|
||||
const dicomData = DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer);
|
||||
|
||||
metadata.study = {
|
||||
accessionNumber: studyMetadata.accessionNumber,
|
||||
patientId: studyMetadata.patientId,
|
||||
studyInstanceUid: studyMetadata.studyInstanceUid,
|
||||
studyDate: studyMetadata.studyDate,
|
||||
studyTime: studyMetadata.studyTime,
|
||||
studyDescription: studyMetadata.studyDescription,
|
||||
institutionName: studyMetadata.institutionName,
|
||||
patientHistory: studyMetadata.patientHistory,
|
||||
};
|
||||
|
||||
metadata.series = {
|
||||
seriesDescription: seriesMetadata.seriesDescription,
|
||||
seriesNumber: seriesMetadata.seriesNumber,
|
||||
seriesDate: seriesMetadata.seriesDate,
|
||||
seriesTime: seriesMetadata.seriesTime,
|
||||
modality: seriesMetadata.modality,
|
||||
seriesInstanceUid: seriesMetadata.seriesInstanceUid,
|
||||
numImages: numImages,
|
||||
};
|
||||
|
||||
metadata.instance = instanceMetadata;
|
||||
|
||||
metadata.patient = {
|
||||
name: studyMetadata.patientName,
|
||||
id: studyMetadata.patientId,
|
||||
birthDate: studyMetadata.patientBirthDate,
|
||||
sex: studyMetadata.patientSex,
|
||||
age: studyMetadata.patientAge,
|
||||
};
|
||||
|
||||
// If there is sufficient information, populate
|
||||
// the imagePlane object for easier use in the Viewer
|
||||
metadata.imagePlane = this.getImagePlane(instanceMetadata);
|
||||
|
||||
// Add the metadata to the imageId lookup object
|
||||
this.metadataLookup.set(imageId, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the metadata for the given imageId
|
||||
* @param {String} imageId The Cornerstone ImageId
|
||||
* @returns image metadata
|
||||
*/
|
||||
getMetadata(imageId) {
|
||||
return this.metadataLookup.get(imageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of metadata to the Cornerstone metadata provider given a specific
|
||||
* imageId, type, and dataset
|
||||
*
|
||||
* @param imageId
|
||||
* @param type (e.g. series, instance, tagDisplay)
|
||||
* @param data
|
||||
*/
|
||||
addSpecificMetadata(imageId, type, data) {
|
||||
const metadata = {};
|
||||
metadata[type] = data;
|
||||
|
||||
const oldMetadata = this.metadataLookup.get(imageId);
|
||||
this.metadataLookup.set(imageId, Object.assign(oldMetadata, metadata));
|
||||
}
|
||||
|
||||
getFromImage(image, type, tag, attrName, defaultValue) {
|
||||
let value;
|
||||
|
||||
if (image.data) {
|
||||
value = this.getFromDataSet(image.data, type, tag);
|
||||
dicomJSONDataset = dicomData.dict;
|
||||
} else {
|
||||
value = image.instance[attrName];
|
||||
dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer;
|
||||
}
|
||||
|
||||
return value === null ? defaultValue : value;
|
||||
}
|
||||
// Check if dataset is already naturalized.
|
||||
|
||||
getFromDataSet(dataSet, type, tag) {
|
||||
if (!dataSet) {
|
||||
return;
|
||||
}
|
||||
let naturalizedDataset;
|
||||
|
||||
const fn = dataSet[type];
|
||||
if (!fn) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fn.call(dataSet, tag);
|
||||
}
|
||||
|
||||
getFrameIncrementPointer(image) {
|
||||
const dataSet = image.data;
|
||||
let frameInstancePointer = '';
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
const frameInstancePointerNames = {
|
||||
x00181063: 'frameTime',
|
||||
x00181065: 'frameTimeVector',
|
||||
};
|
||||
|
||||
// (0028,0009) = Frame Increment Pointer
|
||||
const frameInstancePointerTag = parsingUtils.attributeTag(
|
||||
dataSet,
|
||||
'x00280009'
|
||||
if (dicomJSONDataset['SeriesInstanceUID'] === undefined) {
|
||||
naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomJSONDataset
|
||||
);
|
||||
frameInstancePointer = frameInstancePointerNames[frameInstancePointerTag];
|
||||
} else {
|
||||
frameInstancePointer = image.instance.frameIncrementPointer;
|
||||
naturalizedDataset = dicomJSONDataset;
|
||||
}
|
||||
|
||||
return frameInstancePointer || '';
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
} = naturalizedDataset;
|
||||
|
||||
const study = this._getAndCacheStudy(StudyInstanceUID);
|
||||
const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID);
|
||||
const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID);
|
||||
|
||||
Object.assign(instance, naturalizedDataset);
|
||||
|
||||
await this._checkBulkDataAndInlineBinaries(instance, options.server);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
getFrameTimeVector(image) {
|
||||
const dataSet = image.data;
|
||||
addImageIdToUIDs(imageId, uids) {
|
||||
// This method is a fallback for when you don't have WADO-URI or WADO-RS.
|
||||
// You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here.
|
||||
// An example would be dicom hosted at some random site.
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
|
||||
return parsingUtils.floatArray(dataSet, 'x00181065');
|
||||
this.imageIdToUIDs.set(imageId, uids);
|
||||
}
|
||||
|
||||
_getAndCacheStudy(StudyInstanceUID) {
|
||||
const studies = this.studies;
|
||||
|
||||
let study = studies.get(StudyInstanceUID);
|
||||
|
||||
if (!study) {
|
||||
study = { series: new Map() };
|
||||
studies.set(StudyInstanceUID, study);
|
||||
}
|
||||
|
||||
return image.instance.frameTimeVector;
|
||||
return study;
|
||||
}
|
||||
_getAndCacheSeriesFromStudy(study, SeriesInstanceUID) {
|
||||
let series = study.series.get(SeriesInstanceUID);
|
||||
|
||||
getFrameTime(image) {
|
||||
const dataSet = image.data;
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
// Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
|
||||
// Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
|
||||
// but that case is currently not supported.
|
||||
return dataSet.floatString('x00181063', -1);
|
||||
if (!series) {
|
||||
series = { instances: new Map() };
|
||||
study.series.set(SeriesInstanceUID, series);
|
||||
}
|
||||
|
||||
return image.instance.frameTime;
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the related metadata for missing fields given a specified image
|
||||
*
|
||||
* @param image
|
||||
*/
|
||||
updateMetadata(image) {
|
||||
const imageMetadata = this.metadataLookup.get(image.imageId);
|
||||
if (!imageMetadata) {
|
||||
_getAndCacheInstanceFromStudy(series, SOPInstanceUID) {
|
||||
let instance = series.instances.get(SOPInstanceUID);
|
||||
|
||||
if (!instance) {
|
||||
instance = {};
|
||||
series.instances.set(SOPInstanceUID, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async _checkBulkDataAndInlineBinaries(instance, server) {
|
||||
await fetchOverlayData(instance, server);
|
||||
|
||||
if (instance.PhotometricInterpretation === 'PALETTE COLOR') {
|
||||
await fetchPaletteColorLookupTableData(instance, server);
|
||||
}
|
||||
}
|
||||
|
||||
_getInstance(imageId) {
|
||||
const uids = this._getUIDsFromImageID(imageId);
|
||||
|
||||
if (!uids) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageMetadata.patient.age =
|
||||
imageMetadata.patient.age ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00101010');
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = uids;
|
||||
|
||||
imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows;
|
||||
imageMetadata.instance.columns =
|
||||
imageMetadata.instance.columns || image.columns;
|
||||
|
||||
imageMetadata.instance.sopClassUid =
|
||||
imageMetadata.instance.sopClassUid ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00080016');
|
||||
imageMetadata.instance.sopInstanceUid =
|
||||
imageMetadata.instance.sopInstanceUid ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00080018');
|
||||
|
||||
imageMetadata.instance.pixelSpacing =
|
||||
imageMetadata.instance.pixelSpacing ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00280030');
|
||||
imageMetadata.instance.frameOfReferenceUID =
|
||||
imageMetadata.instance.frameOfReferenceUID ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00200052');
|
||||
imageMetadata.instance.imageOrientationPatient =
|
||||
imageMetadata.instance.imageOrientationPatient ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00200037');
|
||||
imageMetadata.instance.imagePositionPatient =
|
||||
imageMetadata.instance.imagePositionPatient ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00200032');
|
||||
|
||||
imageMetadata.instance.sliceThickness =
|
||||
imageMetadata.instance.sliceThickness ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00180050');
|
||||
imageMetadata.instance.sliceLocation =
|
||||
imageMetadata.instance.sliceLocation ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00201041');
|
||||
imageMetadata.instance.tablePosition =
|
||||
imageMetadata.instance.tablePosition ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00189327');
|
||||
imageMetadata.instance.spacingBetweenSlices =
|
||||
imageMetadata.instance.spacingBetweenSlices ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00180088');
|
||||
|
||||
imageMetadata.instance.lossyImageCompression =
|
||||
imageMetadata.instance.lossyImageCompression ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00282110');
|
||||
imageMetadata.instance.lossyImageCompressionRatio =
|
||||
imageMetadata.instance.lossyImageCompressionRatio ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00282112');
|
||||
|
||||
imageMetadata.instance.frameIncrementPointer =
|
||||
imageMetadata.instance.frameIncrementPointer ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00280009');
|
||||
imageMetadata.instance.frameTime =
|
||||
imageMetadata.instance.frameTime ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00181063');
|
||||
imageMetadata.instance.frameTimeVector =
|
||||
imageMetadata.instance.frameTimeVector ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00181065');
|
||||
|
||||
if (
|
||||
(image.data || image.instance) &&
|
||||
!imageMetadata.instance.multiframeMetadata
|
||||
) {
|
||||
imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(
|
||||
image
|
||||
);
|
||||
}
|
||||
|
||||
imageMetadata.imagePlane =
|
||||
imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and returns the imagePlane given the metadata instance
|
||||
*
|
||||
* @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane
|
||||
* @returns imagePlane The constructed imagePlane to be used in viewer easily
|
||||
*/
|
||||
getImagePlane(instance) {
|
||||
if (
|
||||
!instance.rows ||
|
||||
!instance.columns ||
|
||||
!instance.pixelSpacing ||
|
||||
!instance.frameOfReferenceUID ||
|
||||
!instance.imageOrientationPatient ||
|
||||
!instance.imagePositionPatient
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageOrientation = instance.imageOrientationPatient.split('\\');
|
||||
const imagePosition = instance.imagePositionPatient.split('\\');
|
||||
|
||||
let columnPixelSpacing = 1.0;
|
||||
let rowPixelSpacing = 1.0;
|
||||
if (instance.pixelSpacing) {
|
||||
const split = instance.pixelSpacing.split('\\');
|
||||
rowPixelSpacing = parseFloat(split[0]);
|
||||
columnPixelSpacing = parseFloat(split[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
frameOfReferenceUID: instance.frameOfReferenceUID,
|
||||
rows: instance.rows,
|
||||
columns: instance.columns,
|
||||
rowCosines: [
|
||||
parseFloat(imageOrientation[0]),
|
||||
parseFloat(imageOrientation[1]),
|
||||
parseFloat(imageOrientation[2]),
|
||||
],
|
||||
columnCosines: [
|
||||
parseFloat(imageOrientation[3]),
|
||||
parseFloat(imageOrientation[4]),
|
||||
parseFloat(imageOrientation[5]),
|
||||
],
|
||||
imagePositionPatient: [
|
||||
parseFloat(imagePosition[0]),
|
||||
parseFloat(imagePosition[1]),
|
||||
parseFloat(imagePosition[2]),
|
||||
],
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function extracts miltiframe information from a dicomParser.DataSet object.
|
||||
*
|
||||
* @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
|
||||
* @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
|
||||
*/
|
||||
getMultiframeModuleMetadata(image) {
|
||||
const imageInfo = {
|
||||
isMultiframeImage: false,
|
||||
frameIncrementPointer: null,
|
||||
numberOfFrames: 0,
|
||||
frameTime: 0,
|
||||
frameTimeVector: null,
|
||||
averageFrameRate: 0, // backwards compatibility only... it might be useless in the future
|
||||
};
|
||||
|
||||
let frameTime;
|
||||
|
||||
const numberOfFrames = this.getFromImage(
|
||||
image,
|
||||
'intString',
|
||||
'x00280008',
|
||||
'numberOfFrames',
|
||||
-1
|
||||
return this._getInstanceData(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
|
||||
if (numberOfFrames > 0) {
|
||||
// set multi-frame image indicator
|
||||
imageInfo.isMultiframeImage = true;
|
||||
imageInfo.numberOfFrames = numberOfFrames;
|
||||
|
||||
// (0028,0009) = Frame Increment Pointer
|
||||
const frameIncrementPointer = this.getFrameIncrementPointer(image);
|
||||
|
||||
if (frameIncrementPointer === 'frameTimeVector') {
|
||||
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
|
||||
const frameTimeVector = this.getFrameTimeVector(image);
|
||||
|
||||
if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
|
||||
imageInfo.frameIncrementPointer = frameIncrementPointer;
|
||||
imageInfo.frameTimeVector = frameTimeVector;
|
||||
frameTime =
|
||||
frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
|
||||
imageInfo.averageFrameRate = 1000 / frameTime;
|
||||
}
|
||||
} else if (
|
||||
frameIncrementPointer === 'frameTime' ||
|
||||
frameIncrementPointer === ''
|
||||
) {
|
||||
frameTime = this.getFrameTime(image);
|
||||
|
||||
if (frameTime > 0) {
|
||||
imageInfo.frameIncrementPointer = frameIncrementPointer;
|
||||
imageInfo.frameTime = frameTime;
|
||||
imageInfo.averageFrameRate = 1000 / frameTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bound reference to the provider function.
|
||||
*/
|
||||
getProvider() {
|
||||
let provider = this._provider;
|
||||
if (typeof this._provider !== FUNCTION) {
|
||||
provider = this.provider.bind(this);
|
||||
this._provider = provider;
|
||||
get(query, imageId, options = { fallback: false }) {
|
||||
const instance = this._getInstance(imageId);
|
||||
|
||||
if (query === INSTANCE) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
return provider;
|
||||
return this.getTagFromInstance(query, instance, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up metadata for Cornerstone Tools given a specified type and imageId
|
||||
* A type may be, e.g. 'study', or 'patient', or 'imagePlaneModule'. These types
|
||||
* are keys in the stored metadata objects.
|
||||
*
|
||||
* @param type
|
||||
* @param imageId
|
||||
* @returns {Object} Relevant metadata of the specified type
|
||||
*/
|
||||
provider(type, imageId) {
|
||||
const imageMetadata = this.metadataLookup.get(imageId);
|
||||
if (!imageMetadata) {
|
||||
getTag(query, imageId, options) {
|
||||
return this.get(query, imageId, options);
|
||||
}
|
||||
|
||||
getInstance(imageId) {
|
||||
return this.get(INSTANCE, imageId);
|
||||
}
|
||||
|
||||
getTagFromInstance(
|
||||
naturalizedTagOrWADOImageLoaderTag,
|
||||
instance,
|
||||
options = { fallback: false }
|
||||
) {
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageMetadata.hasOwnProperty(type)) {
|
||||
return imageMetadata[type];
|
||||
// If its a naturalized dcmjs tag present on the instance, return.
|
||||
if (instance[naturalizedTagOrWADOImageLoaderTag]) {
|
||||
return instance[naturalizedTagOrWADOImageLoaderTag];
|
||||
}
|
||||
|
||||
// Maybe its a legacy CornerstoneWADOImageLoader tag then:
|
||||
return this._getCornerstoneWADOImageLoaderTag(
|
||||
naturalizedTagOrWADOImageLoaderTag,
|
||||
instance
|
||||
);
|
||||
}
|
||||
|
||||
_getCornerstoneWADOImageLoaderTag(wadoImageLoaderTag, instance) {
|
||||
let metadata;
|
||||
|
||||
switch (wadoImageLoaderTag) {
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE:
|
||||
const { SeriesDate, SeriesTime } = instance;
|
||||
|
||||
let seriesDate;
|
||||
let seriesTime;
|
||||
|
||||
if (SeriesDate) {
|
||||
seriesDate = dicomParser.parseDA(SeriesDate);
|
||||
}
|
||||
|
||||
if (SeriesTime) {
|
||||
seriesTime = dicomParser.parseTM(SeriesTime);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
modality: instance.Modality,
|
||||
seriesInstanceUID: instance.SeriesInstanceUID,
|
||||
seriesNumber: instance.SeriesNumber,
|
||||
studyInstanceUID: instance.StudyInstanceUID,
|
||||
seriesDate,
|
||||
seriesTime,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
||||
metadata = {
|
||||
patientAge: instance.PatientAge,
|
||||
patientSize: instance.PatientSize,
|
||||
patientWeight: instance.PatientWeight,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
|
||||
const { ImageOrientationPatient } = instance;
|
||||
|
||||
// Fallback for DX images.
|
||||
// TODO: We should use the rest of the results of this function
|
||||
// to update the UI somehow
|
||||
const { PixelSpacing } = getPixelSpacingInformation(instance);
|
||||
|
||||
let rowPixelSpacing;
|
||||
let columnPixelSpacing;
|
||||
|
||||
let rowCosines;
|
||||
let columnCosines;
|
||||
|
||||
if (PixelSpacing) {
|
||||
rowPixelSpacing = PixelSpacing[0];
|
||||
columnPixelSpacing = PixelSpacing[1];
|
||||
}
|
||||
|
||||
if (ImageOrientationPatient) {
|
||||
rowCosines = ImageOrientationPatient.slice(0, 3);
|
||||
columnCosines = ImageOrientationPatient.slice(3, 6);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
imageOrientationPatient: ImageOrientationPatient,
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: instance.ImagePositionPatient,
|
||||
sliceThickness: instance.SliceThickness,
|
||||
sliceLocation: instance.SliceLocation,
|
||||
pixelSpacing: PixelSpacing,
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
|
||||
metadata = {
|
||||
samplesPerPixel: instance.SamplesPerPixel,
|
||||
photometricInterpretation: instance.PhotometricInterpretation,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
bitsAllocated: instance.BitsAllocated,
|
||||
bitsStored: instance.BitsStored,
|
||||
highBit: instance.HighBit,
|
||||
pixelRepresentation: instance.PixelRepresentation,
|
||||
planarConfiguration: instance.PlanarConfiguration,
|
||||
pixelAspectRatio: instance.PixelAspectRatio,
|
||||
smallestPixelValue: instance.SmallestPixelValue,
|
||||
largestPixelValue: instance.LargestPixelValue,
|
||||
redPaletteColorLookupTableDescriptor:
|
||||
instance.RedPaletteColorLookupTableDescriptor,
|
||||
greenPaletteColorLookupTableDescriptor:
|
||||
instance.GreenPaletteColorLookupTableDescriptor,
|
||||
bluePaletteColorLookupTableDescriptor:
|
||||
instance.BluePaletteColorLookupTableDescriptor,
|
||||
redPaletteColorLookupTableData:
|
||||
instance.RedPaletteColorLookupTableData,
|
||||
greenPaletteColorLookupTableData:
|
||||
instance.GreenPaletteColorLookupTableData,
|
||||
bluePaletteColorLookupTableData:
|
||||
instance.BluePaletteColorLookupTableData,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||
const { WindowCenter, WindowWidth } = instance;
|
||||
|
||||
const windowCenter = Array.isArray(WindowCenter)
|
||||
? WindowCenter
|
||||
: [WindowCenter];
|
||||
const windowWidth = Array.isArray(WindowWidth)
|
||||
? WindowWidth
|
||||
: [WindowWidth];
|
||||
|
||||
metadata = {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||
metadata = {
|
||||
rescaleIntercept: instance.RescaleIntercept,
|
||||
rescaleSlope: instance.RescaleSlope,
|
||||
rescaleType: instance.RescaleType,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.SOP_COMMON_MODULE:
|
||||
metadata = {
|
||||
sopClassUID: instance.SOPClassUID,
|
||||
sopInstanceUID: instance.SOPInstanceUID,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE:
|
||||
const { RadiopharmaceuticalInformationSequence } = instance;
|
||||
|
||||
if (RadiopharmaceuticalInformationSequence) {
|
||||
const RadiopharmaceuticalInformation = Array.isArray(
|
||||
RadiopharmaceuticalInformationSequence
|
||||
)
|
||||
? RadiopharmaceuticalInformationSequence[0]
|
||||
: RadiopharmaceuticalInformationSequence;
|
||||
|
||||
const {
|
||||
RadiopharmaceuticalStartTime,
|
||||
RadionuclideTotalDose,
|
||||
RadionuclideHalfLife,
|
||||
} = RadiopharmaceuticalInformation;
|
||||
|
||||
const radiopharmaceuticalInfo = {
|
||||
radiopharmaceuticalStartTime: dicomParser.parseTM(
|
||||
RadiopharmaceuticalStartTime
|
||||
),
|
||||
radionuclideTotalDose: RadionuclideTotalDose,
|
||||
radionuclideHalfLife: RadionuclideHalfLife,
|
||||
};
|
||||
metadata = {
|
||||
radiopharmaceuticalInfo,
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.OVERLAY_PLANE_MODULE:
|
||||
const overlays = [];
|
||||
|
||||
for (
|
||||
let overlayGroup = 0x00;
|
||||
overlayGroup <= 0x1e;
|
||||
overlayGroup += 0x02
|
||||
) {
|
||||
let groupStr = `60${overlayGroup.toString(16)}`;
|
||||
|
||||
if (groupStr.length === 3) {
|
||||
groupStr = `600${overlayGroup.toString(16)}`;
|
||||
}
|
||||
|
||||
const OverlayDataTag = `${groupStr}3000`;
|
||||
const OverlayData = instance[OverlayDataTag];
|
||||
|
||||
if (!OverlayData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const OverlayRowsTag = `${groupStr}0010`;
|
||||
const OverlayColumnsTag = `${groupStr}0011`;
|
||||
const OverlayType = `${groupStr}0040`;
|
||||
const OverlayOriginTag = `${groupStr}0050`;
|
||||
const OverlayDescriptionTag = `${groupStr}0022`;
|
||||
const OverlayLabelTag = `${groupStr}1500`;
|
||||
const ROIAreaTag = `${groupStr}1301`;
|
||||
const ROIMeanTag = `${groupStr}1302`;
|
||||
const ROIStandardDeviationTag = `${groupStr}1303`;
|
||||
const OverlayOrigin = instance[OverlayOriginTag];
|
||||
|
||||
const overlay = {
|
||||
rows: instance[OverlayRowsTag],
|
||||
columns: instance[OverlayColumnsTag],
|
||||
type: instance[OverlayType],
|
||||
x: OverlayOrigin[0],
|
||||
y: OverlayOrigin[1],
|
||||
pixelData: OverlayData,
|
||||
description: instance[OverlayDescriptionTag],
|
||||
label: instance[OverlayLabelTag],
|
||||
roiArea: instance[ROIAreaTag],
|
||||
roiMean: instance[ROIMeanTag],
|
||||
roiStandardDeviation: instance[ROIStandardDeviationTag],
|
||||
};
|
||||
|
||||
overlays.push(overlay);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
overlays
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE:
|
||||
const { PatientName } = instance;
|
||||
|
||||
let patientName;
|
||||
if (PatientName) {
|
||||
patientName = PatientName.Alphabetic;
|
||||
}
|
||||
|
||||
metadata = {
|
||||
patientName,
|
||||
patientId: instance.PatientID,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||
metadata = {
|
||||
instanceNumber: instance.InstanceNumber,
|
||||
lossyImageCompression: instance.LossyImageCompression,
|
||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE:
|
||||
metadata = {
|
||||
studyDescription: instance.StudyDescription,
|
||||
studyDate: instance.StudyDate,
|
||||
studyTime: instance.StudyTime,
|
||||
accessionNumber: instance.AccessionNumber,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.CINE_MODULE:
|
||||
metadata = {
|
||||
frameTime: instance.FrameTime,
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
_getInstanceData(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
const study = this.studies.get(StudyInstanceUID);
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
const series = study.series.get(SeriesInstanceUID);
|
||||
|
||||
if (!series) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = series.instances.get(SOPInstanceUID);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
_getUIDsFromImageID(imageId) {
|
||||
if (imageId.includes('wadors:')) {
|
||||
const strippedImageId = imageId.split('studies/')[1];
|
||||
const splitImageId = strippedImageId.split('/');
|
||||
|
||||
return {
|
||||
StudyInstanceUID: splitImageId[0], // Note: splitImageId[1] === 'series'
|
||||
SeriesInstanceUID: splitImageId[2], // Note: splitImageId[3] === 'instances'
|
||||
SOPInstanceUID: splitImageId[4],
|
||||
};
|
||||
}
|
||||
if (imageId.includes('wado?requestType=WADO')) {
|
||||
const qs = queryString.parse(imageId);
|
||||
|
||||
return {
|
||||
StudyInstanceUID: qs.studyUID,
|
||||
SeriesInstanceUID: qs.seriesUID,
|
||||
SOPInstanceUID: qs.objectUID,
|
||||
};
|
||||
} else {
|
||||
// Maybe its a non-standard imageId
|
||||
return this.imageIdToUIDs.get(imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataProvider;
|
||||
const metadataProvider = new MetadataProvider();
|
||||
|
||||
export default metadataProvider;
|
||||
|
||||
const WADO_IMAGE_LOADER_TAGS = {
|
||||
// CornerstoneWADOImageLoader specific
|
||||
GENERAL_SERIES_MODULE: 'generalSeriesModule',
|
||||
PATIENT_STUDY_MODULE: 'patientStudyModule',
|
||||
IMAGE_PLANE_MODULE: 'imagePlaneModule',
|
||||
IMAGE_PIXEL_MODULE: 'imagePixelModule',
|
||||
VOI_LUT_MODULE: 'voiLutModule',
|
||||
MODALITY_LUT_MODULE: 'modalityLutModule',
|
||||
SOP_COMMON_MODULE: 'sopCommonModule',
|
||||
PET_ISOTOPE_MODULE: 'petIsotopeModule',
|
||||
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
|
||||
|
||||
// react-cornerstone-viewport specifc
|
||||
PATIENT_MODULE: 'patientModule',
|
||||
GENERAL_IMAGE_MODULE: 'generalImageModule',
|
||||
GENERAL_STUDY_MODULE: 'generalStudyModule',
|
||||
CINE_MODULE: 'cineModule',
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
@@ -1,9 +1,8 @@
|
||||
import { studyMetadataManager, updateMetaDataManager } from '../utils';
|
||||
import { studyMetadataManager } from '../utils';
|
||||
|
||||
import OHIFError from './OHIFError';
|
||||
import { StudyMetadata } from './metadata/StudyMetadata';
|
||||
import { StudyMetadataSource } from './StudyMetadataSource.js';
|
||||
import { StudySummary } from './metadata/StudySummary';
|
||||
import { retrieveStudyMetadata } from '../studies/retrieveStudyMetadata.js';
|
||||
|
||||
export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
@@ -19,12 +18,12 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
|
||||
/**
|
||||
* Load study info (OHIF.viewer.Studies) and study metadata (OHIF.viewer.StudyMetadataList) for a given study.
|
||||
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
|
||||
* @param {StudyMetadata} study StudyMetadata object.
|
||||
*/
|
||||
loadStudy(study) {
|
||||
if (!(study instanceof StudyMetadata) && !(study instanceof StudySummary)) {
|
||||
if (!(study instanceof StudyMetadata)) {
|
||||
throw new OHIFError(
|
||||
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudySummary or StudyMetadata'
|
||||
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudyMetadata'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +32,7 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
|
||||
if (study instanceof StudyMetadata) {
|
||||
const alreadyLoaded = OHIF.viewer.Studies.findBy({
|
||||
studyInstanceUid: studyInstanceUID,
|
||||
StudyInstanceUID: studyInstanceUID,
|
||||
});
|
||||
|
||||
if (!alreadyLoaded) {
|
||||
@@ -49,7 +48,7 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
// Create study metadata object
|
||||
const studyMetadata = new StudyMetadata(
|
||||
studyInfo,
|
||||
studyInfo.studyInstanceUid
|
||||
studyInfo.StudyInstanceUID
|
||||
);
|
||||
|
||||
// Get Study display sets
|
||||
@@ -72,10 +71,6 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
// Set some studyInfo properties
|
||||
studyInfo.selected = true;
|
||||
studyInfo.displaySets = studyMetadata.getDisplaySets();
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(studyInfo);
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import StudySummary from './StudySummary';
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const STRING = 'string';
|
||||
const propertyReplacementMap = {
|
||||
modalities: 'ModalitiesInStudy',
|
||||
patientBirthdate: 'PatientBirthDate',
|
||||
};
|
||||
|
||||
/**
|
||||
* OHIF Viewers specialized version of StudySummary class
|
||||
*/
|
||||
export class OHIFStudySummary extends StudySummary {
|
||||
// @Override
|
||||
addTags(tagMap) {
|
||||
const _hasOwn = Object.prototype.hasOwnProperty;
|
||||
const _tagMap = Object.create(null);
|
||||
for (let property in tagMap) {
|
||||
if (_hasOwn.call(tagMap, property)) {
|
||||
let standardProperty = OHIFStudySummary.getStandardPropertyName(
|
||||
property
|
||||
);
|
||||
if (standardProperty) {
|
||||
_tagMap[standardProperty] = tagMap[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
super.addTags(_tagMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a non-standard, OHIF specific, DICOM property name into a standard one.
|
||||
* @param {string} property A string representing a non-conforming keyword.
|
||||
* @returns {string|undefined} Returns a standard-conforming property name.
|
||||
*/
|
||||
static getStandardPropertyName(property) {
|
||||
let standardProperty;
|
||||
if (typeof property === STRING && property.charAt(0) !== '_') {
|
||||
if (property in propertyReplacementMap) {
|
||||
standardProperty = propertyReplacementMap[propertyReplacementMap];
|
||||
} else {
|
||||
standardProperty = property
|
||||
.replace(/^sop/, 'SOP')
|
||||
.replace(/Uid$/, 'UID')
|
||||
.replace(/Id$/, 'ID');
|
||||
standardProperty =
|
||||
standardProperty.charAt(0).toUpperCase() + standardProperty.substr(1);
|
||||
}
|
||||
}
|
||||
return standardProperty;
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,8 @@ class BaseLoadingListener {
|
||||
}
|
||||
|
||||
_getProgressId() {
|
||||
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
|
||||
return 'StackProgress:' + displaySetInstanceUid;
|
||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||
return 'StackProgress:' + displaySetInstanceUID;
|
||||
}
|
||||
|
||||
_clearProgress() {
|
||||
@@ -362,7 +362,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
|
||||
_logProgress() {
|
||||
const totalFramesCount = this.stack.imageIds.length;
|
||||
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
|
||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||
let progressBar = '[';
|
||||
|
||||
for (let i = 0; i < totalFramesCount; i++) {
|
||||
@@ -371,7 +371,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
}
|
||||
|
||||
progressBar += ']';
|
||||
log.info(`${displaySetInstanceUid}: ${progressBar}`);
|
||||
log.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,12 +388,12 @@ class StudyLoadingListener {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySetInstanceUid = stack.displaySetInstanceUid;
|
||||
const displaySetInstanceUID = stack.displaySetInstanceUID;
|
||||
|
||||
if (!this.listeners[displaySetInstanceUid]) {
|
||||
if (!this.listeners[displaySetInstanceUID]) {
|
||||
const listener = this._createListener(stack, stackMetaData);
|
||||
if (listener) {
|
||||
this.listeners[displaySetInstanceUid] = listener;
|
||||
this.listeners[displaySetInstanceUID] = listener;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,12 +424,12 @@ class StudyLoadingListener {
|
||||
}
|
||||
|
||||
clear() {
|
||||
const displaySetInstanceUids = Object.keys(this.listeners);
|
||||
const length = displaySetInstanceUids.length;
|
||||
const displaySetInstanceUIDs = Object.keys(this.listeners);
|
||||
const length = displaySetInstanceUIDs.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const displaySetInstanceUid = displaySetInstanceUids[i];
|
||||
const displaySet = this.listeners[displaySetInstanceUid];
|
||||
const displaySetInstanceUID = displaySetInstanceUIDs[i];
|
||||
const displaySet = this.listeners[displaySetInstanceUID];
|
||||
|
||||
displaySet.destroy();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class StudyMetadataSource {
|
||||
|
||||
/**
|
||||
* Load study info and study metadata for a given study into the viewer.
|
||||
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
|
||||
* @param {StudyMetadata} study StudyMetadata object.
|
||||
*/
|
||||
loadStudy(study) {
|
||||
/**
|
||||
|
||||
@@ -94,17 +94,23 @@ export class StudyPrefetcher {
|
||||
}
|
||||
|
||||
getStudy(image) {
|
||||
const studyMetadata = cornerstone.metaData.get('study', image.imageId);
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
return OHIF.viewer.Studies.find(
|
||||
study => study.studyInstanceUid === studyMetadata.studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
getSeries(study, image) {
|
||||
const seriesMetadata = cornerstone.metaData.get('series', image.imageId);
|
||||
const SeriesInstanceUID = cornerstone.metaData.get(
|
||||
'SeriesInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
||||
|
||||
return studyMetadata.getSeriesByUID(seriesMetadata.seriesInstanceUid);
|
||||
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
getInstance(series, image) {
|
||||
@@ -112,13 +118,13 @@ export class StudyPrefetcher {
|
||||
'instance',
|
||||
image.imageId
|
||||
);
|
||||
return series.getInstanceByUID(instanceMetadata.sopInstanceUid);
|
||||
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
|
||||
}
|
||||
|
||||
getActiveDisplaySet(displaySets, instance) {
|
||||
return displaySets.find(displaySet => {
|
||||
return displaySet.images.some(displaySetImage => {
|
||||
return displaySetImage.sopInstanceUid === instance.sopInstanceUid;
|
||||
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Only imported in environment w/ `window`
|
||||
// So we need to mock these for tests
|
||||
import Mousetrap from 'mousetrap';
|
||||
import pausePlugin from 'mousetrap/plugins/pause/mousetrap-pause.js';
|
||||
import recordPlugin from 'mousetrap/plugins/record/mousetrap-record.js';
|
||||
|
||||
// import pausePlugin from './pausePlugin.js';
|
||||
// import recordPlugin from './recordPlugin.js';
|
||||
|
||||
// // // TODO: May need to bind these so Mousetrap = this in plugins;
|
||||
// pausePlugin(Mousetrap);
|
||||
// recordPlugin(Mousetrap);
|
||||
|
||||
// console.log(Mousetrap);
|
||||
// console.log(Object.keys(Mousetrap));
|
||||
|
||||
export default Mousetrap;
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
InstanceMetadata,
|
||||
SeriesMetadata,
|
||||
StudyMetadata,
|
||||
StudySummary,
|
||||
} from './metadata';
|
||||
import { InstanceMetadata, SeriesMetadata, StudyMetadata } from './metadata';
|
||||
|
||||
import CommandsManager from './CommandsManager.js';
|
||||
import { DICOMFileLoadingListener } from './StudyLoadingListener';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import ImageSet from './ImageSet';
|
||||
import MetadataProvider from './MetadataProvider.js';
|
||||
import MetadataProvider from './MetadataProvider';
|
||||
import OHIFError from './OHIFError.js';
|
||||
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
|
||||
import { StackLoadingListener } from './StudyLoadingListener';
|
||||
@@ -31,7 +26,6 @@ export {
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
StudySummary,
|
||||
TypeSafeCollection,
|
||||
OHIFError,
|
||||
StudyMetadataSource,
|
||||
@@ -50,7 +44,6 @@ const classes = {
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
StudySummary,
|
||||
TypeSafeCollection,
|
||||
OHIFError,
|
||||
StudyMetadataSource,
|
||||
|
||||
Loaded 100 of 341 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user