Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c5223fcf1 | ||
|
|
8513ad9ef3 | ||
|
|
66d8bc6d6f | ||
|
|
0064a4b1cd | ||
|
|
74ce89b22b | ||
|
|
c92975f555 | ||
|
|
ce6c0533a0 | ||
|
|
4df624b8c4 | ||
|
|
e064fd353e | ||
|
|
458d310c78 | ||
|
|
73bc4d9094 | ||
|
|
7709ebb107 | ||
|
|
559b637290 | ||
|
|
36910f2b47 | ||
|
|
9e10c2b2de | ||
|
|
c5cbd77cb9 | ||
|
|
b3b4c10e4c | ||
|
|
855020d730 | ||
|
|
8503cbce4e | ||
|
|
c8667fb2d6 | ||
|
|
674ca9f96f | ||
|
|
7a4b97b4e8 | ||
|
|
c79c0c301d | ||
|
|
ff5060f276 | ||
|
|
dafb26b34b | ||
|
|
c4f9d5e750 | ||
|
|
cf40a8388c | ||
|
|
d596ab5c3d | ||
|
|
72198eee7d | ||
|
|
057f68a9b4 | ||
|
|
034ca64d6b |
No files matched your search
@@ -175,7 +175,7 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: 'Build Demo Site'
|
name: 'Build Demo Site'
|
||||||
command: yarn run build:demo
|
command: yarn run build:demo
|
||||||
no_output_timeout: 30m
|
no_output_timeout: 45m
|
||||||
- run:
|
- run:
|
||||||
name: 'Upload SourceMaps, Send Deploy Notification'
|
name: 'Upload SourceMaps, Send Deploy Notification'
|
||||||
command: |
|
command: |
|
||||||
|
|||||||
+3
-1
@@ -8,11 +8,13 @@ coverage:
|
|||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
threshold: 0.10%
|
threshold: 0.5%
|
||||||
core:
|
core:
|
||||||
flags: core
|
flags: core
|
||||||
|
threshold: 0.5%
|
||||||
viewer:
|
viewer:
|
||||||
flags: viewer
|
flags: viewer
|
||||||
|
threshold: 0.5%
|
||||||
patch: off
|
patch: off
|
||||||
flags:
|
flags:
|
||||||
core:
|
core:
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ deployment recipe.
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
- [Yarn 1.17.3+](https://yarnpkg.com/en/docs/install)
|
||||||
- [Node 8+](https://nodejs.org/en/)
|
- [Node 10+](https://nodejs.org/en/)
|
||||||
- Yarn Workspaces should be enabled on your machine:
|
- Yarn Workspaces should be enabled on your machine:
|
||||||
- `yarn config set workspaces-experimental true`
|
- `yarn config set workspaces-experimental true`
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,82 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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)
|
## [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
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "0.50.22",
|
"version": "0.52.2",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"react-vtkjs-viewport": "0.1.0"
|
"react-vtkjs-viewport": "0.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^1.0.1",
|
"@ohif/core": "^1.2.0",
|
||||||
"@ohif/ui": "^0.51.2",
|
"@ohif/ui": "^0.53.1",
|
||||||
"cornerstone-tools": "^4.0.9",
|
"cornerstone-tools": "^4.0.9",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.6.1",
|
"dcmjs": "^0.6.1",
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
import { getImageData, loadImageData } from "react-vtkjs-viewport";
|
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||||
|
|
||||||
import ConnectedVTKViewport from "./ConnectedVTKViewport";
|
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||||
import LoadingIndicator from "./LoadingIndicator.js";
|
import LoadingIndicator from './LoadingIndicator.js';
|
||||||
import OHIF from "@ohif/core";
|
import OHIF from '@ohif/core';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from "cornerstone-core";
|
import cornerstone from 'cornerstone-core';
|
||||||
import handleSegmentationStorage from "./handleSegmentationStorage.js";
|
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||||
import vtkDataArray from "vtk.js/Sources/Common/Core/DataArray";
|
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
|
||||||
import vtkImageData from "vtk.js/Sources/Common/DataModel/ImageData";
|
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
|
||||||
import vtkVolume from "vtk.js/Sources/Rendering/Core/Volume";
|
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||||
import vtkVolumeMapper from "vtk.js/Sources/Rendering/Core/VolumeMapper";
|
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||||
|
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ cornerstone.metaData.addProvider(
|
|||||||
StackManager.setMetadataProvider(metadataProvider);
|
StackManager.setMetadataProvider(metadataProvider);
|
||||||
|
|
||||||
const SOP_CLASSES = {
|
const SOP_CLASSES = {
|
||||||
SEGMENTATION_STORAGE: "1.2.840.10008.5.1.4.1.1.66.4"
|
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||||
};
|
};
|
||||||
|
|
||||||
const specialCaseHandlers = {};
|
const specialCaseHandlers = {};
|
||||||
@@ -42,7 +43,7 @@ const volumeCache = {};
|
|||||||
*/
|
*/
|
||||||
function createLabelMapImageData(backgroundImageData) {
|
function createLabelMapImageData(backgroundImageData) {
|
||||||
const labelMapData = vtkImageData.newInstance(
|
const labelMapData = vtkImageData.newInstance(
|
||||||
backgroundImageData.get("spacing", "origin", "direction")
|
backgroundImageData.get('spacing', 'origin', 'direction')
|
||||||
);
|
);
|
||||||
labelMapData.setDimensions(backgroundImageData.getDimensions());
|
labelMapData.setDimensions(backgroundImageData.getDimensions());
|
||||||
labelMapData.computeTransforms();
|
labelMapData.computeTransforms();
|
||||||
@@ -50,7 +51,7 @@ function createLabelMapImageData(backgroundImageData) {
|
|||||||
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
|
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
|
||||||
const dataArray = vtkDataArray.newInstance({
|
const dataArray = vtkDataArray.newInstance({
|
||||||
numberOfComponents: 1, // labelmap with single component
|
numberOfComponents: 1, // labelmap with single component
|
||||||
values
|
values,
|
||||||
});
|
});
|
||||||
labelMapData.getPointData().setScalars(dataArray);
|
labelMapData.getPointData().setScalars(dataArray);
|
||||||
|
|
||||||
@@ -61,24 +62,24 @@ class OHIFVTKViewport extends Component {
|
|||||||
state = {
|
state = {
|
||||||
volumes: null,
|
volumes: null,
|
||||||
paintFilterLabelMapImageData: null,
|
paintFilterLabelMapImageData: null,
|
||||||
paintFilterBackgroundImageData: null
|
paintFilterBackgroundImageData: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
studies: PropTypes.object,
|
studies: PropTypes.object,
|
||||||
displaySet: PropTypes.object,
|
displaySet: PropTypes.object,
|
||||||
viewportIndex: PropTypes.number,
|
viewportIndex: PropTypes.number,
|
||||||
children: PropTypes.node
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = "OHIFVTKViewport";
|
static id = 'OHIFVTKViewport';
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
console.log("OHIFVTKViewport init()");
|
console.log('OHIFVTKViewport init()');
|
||||||
}
|
}
|
||||||
|
|
||||||
static destroy() {
|
static destroy() {
|
||||||
console.log("OHIFVTKViewport destroy()");
|
console.log('OHIFVTKViewport destroy()');
|
||||||
StackManager.clearStacks();
|
StackManager.clearStacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
} else if (sopInstanceUid) {
|
} else if (sopInstanceUid) {
|
||||||
const index = stack.imageIds.findIndex(imageId => {
|
const index = stack.imageIds.findIndex(imageId => {
|
||||||
const sopCommonModule = cornerstone.metaData.get(
|
const sopCommonModule = cornerstone.metaData.get(
|
||||||
"sopCommonModule",
|
'sopCommonModule',
|
||||||
imageId
|
imageId
|
||||||
);
|
);
|
||||||
if (!sopCommonModule) {
|
if (!sopCommonModule) {
|
||||||
@@ -151,7 +152,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
|
|
||||||
switch (sopClassUid) {
|
switch (sopClassUid) {
|
||||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||||
throw new Error("Not yet implemented");
|
throw new Error('Not yet implemented');
|
||||||
|
|
||||||
const data = handleSegmentationStorage(
|
const data = handleSegmentationStorage(
|
||||||
stack.imageIds,
|
stack.imageIds,
|
||||||
@@ -164,7 +165,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
return loadImageData(imageDataObject).then(() => {
|
return loadImageData(imageDataObject).then(() => {
|
||||||
return {
|
return {
|
||||||
data: imageDataObject.vtkImageData,
|
data: imageDataObject.vtkImageData,
|
||||||
labelmap: labelmapDataObject
|
labelmap: labelmapDataObject,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
@@ -172,7 +173,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
|
|
||||||
return loadImageData(imageDataObject).then(() => {
|
return loadImageData(imageDataObject).then(() => {
|
||||||
return {
|
return {
|
||||||
data: imageDataObject.vtkImageData
|
data: imageDataObject.vtkImageData,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -189,10 +190,16 @@ class OHIFVTKViewport extends Component {
|
|||||||
volumeActor.setMapper(volumeMapper);
|
volumeActor.setMapper(volumeMapper);
|
||||||
volumeMapper.setInputData(data);
|
volumeMapper.setInputData(data);
|
||||||
|
|
||||||
const range = data.getPointData().getScalars().getRange();
|
const range = data
|
||||||
|
.getPointData()
|
||||||
|
.getScalars()
|
||||||
|
.getRange();
|
||||||
|
|
||||||
// TODO: For PET we might want to just set this to 0-5 SUV
|
// TODO: For PET we might want to just set this to 0-5 SUV
|
||||||
volumeActor.getProperty().getRGBTransferFunction(0).setRange(range[0], range[1]);
|
volumeActor
|
||||||
|
.getProperty()
|
||||||
|
.getRGBTransferFunction(0)
|
||||||
|
.setRange(range[0], range[1]);
|
||||||
|
|
||||||
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
|
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
|
||||||
const sampleDistance =
|
const sampleDistance =
|
||||||
@@ -200,7 +207,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
Math.sqrt(
|
Math.sqrt(
|
||||||
data
|
data
|
||||||
.getSpacing()
|
.getSpacing()
|
||||||
.map((v) => v * v)
|
.map(v => v * v)
|
||||||
.reduce((a, b) => a + b, 0)
|
.reduce((a, b) => a + b, 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -218,12 +225,12 @@ class OHIFVTKViewport extends Component {
|
|||||||
displaySetInstanceUid,
|
displaySetInstanceUid,
|
||||||
sopClassUids,
|
sopClassUids,
|
||||||
sopInstanceUid,
|
sopInstanceUid,
|
||||||
frameIndex
|
frameIndex,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
|
|
||||||
if (sopClassUids.length > 1) {
|
if (sopClassUids.length > 1) {
|
||||||
console.warn(
|
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.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +256,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
volumes: [volumeActor],
|
volumes: [volumeActor],
|
||||||
paintFilterBackgroundImageData: data,
|
paintFilterBackgroundImageData: data,
|
||||||
paintFilterLabelMapImageData: labelmap
|
paintFilterLabelMapImageData: labelmap,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +264,11 @@ class OHIFVTKViewport extends Component {
|
|||||||
this.setStateFromProps();
|
this.setStateFromProps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
console.log(this.props.viewportIndex);
|
||||||
|
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { studies, displaySet } = this.props.viewportData;
|
const { studies, displaySet } = this.props.viewportData;
|
||||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||||
@@ -279,12 +291,12 @@ class OHIFVTKViewport extends Component {
|
|||||||
childrenWithProps = this.props.children.map((child, index) => {
|
childrenWithProps = this.props.children.map((child, index) => {
|
||||||
return React.cloneElement(child, {
|
return React.cloneElement(child, {
|
||||||
viewportIndex: this.props.viewportIndex,
|
viewportIndex: this.props.viewportIndex,
|
||||||
key: index
|
key: index,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = { width: "100%", height: "100%", position: "relative" };
|
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import {
|
|||||||
vtkInteractorStyleMPRCrosshairs,
|
vtkInteractorStyleMPRCrosshairs,
|
||||||
vtkInteractorStyleMPRWindowLevel,
|
vtkInteractorStyleMPRWindowLevel,
|
||||||
vtkInteractorStyleMPRSlice,
|
vtkInteractorStyleMPRSlice,
|
||||||
|
vtkInteractorStyleMPRRotate,
|
||||||
vtkSVGCrosshairsWidget,
|
vtkSVGCrosshairsWidget,
|
||||||
vtkSVGWidgetManager,
|
vtkSVGWidgetManager,
|
||||||
} from 'react-vtkjs-viewport';
|
} from 'react-vtkjs-viewport';
|
||||||
|
|
||||||
import setMPRLayout from './utils/setMPRLayout.js';
|
import setMPRLayout from './utils/setMPRLayout.js';
|
||||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||||
|
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
|
||||||
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||||
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
|
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
|
||||||
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
|
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
|
||||||
@@ -18,6 +20,7 @@ const { BlendMode } = Constants;
|
|||||||
// TODO: Put this somewhere else
|
// TODO: Put this somewhere else
|
||||||
let apis = {};
|
let apis = {};
|
||||||
let currentSlabThickness = 0.1;
|
let currentSlabThickness = 0.1;
|
||||||
|
let defaultSlabThickness = 0.1;
|
||||||
|
|
||||||
function getCrosshairCallbackForIndex(index) {
|
function getCrosshairCallbackForIndex(index) {
|
||||||
return ({ worldPos }) => {
|
return ({ worldPos }) => {
|
||||||
@@ -98,7 +101,7 @@ function _setView(api, sliceNormal, viewUp) {
|
|||||||
const camera = renderer.getActiveCamera();
|
const camera = renderer.getActiveCamera();
|
||||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||||
istyle.setSliceNormal(...sliceNormal);
|
istyle.setSliceNormal(...sliceNormal);
|
||||||
camera.setViewUp(...viewUp);
|
istyle.setViewUp(...viewUp);
|
||||||
|
|
||||||
renderWindow.render();
|
renderWindow.render();
|
||||||
}
|
}
|
||||||
@@ -109,14 +112,15 @@ function switchMPRInteractors(api, istyle) {
|
|||||||
const camera = renderer.getActiveCamera();
|
const camera = renderer.getActiveCamera();
|
||||||
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
|
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
|
||||||
|
|
||||||
let currentNormal;
|
let currentViewport;
|
||||||
if (currentIStyle.getSliceNormal && istyle.getSliceNormal) {
|
if (currentIStyle.getViewport && istyle.getViewport) {
|
||||||
currentNormal = currentIStyle.getSliceNormal();
|
currentViewport = currentIStyle.getViewport();
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentSlabThickness;
|
let currentSlabThickness;
|
||||||
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
|
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
|
||||||
currentSlabThickness = currentIStyle.getSlabThickness();
|
currentSlabThickness = currentIStyle.getSlabThickness();
|
||||||
|
defaultSlabThickness = currentSlabThickness;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
renderWindow.getInteractor().setInteractorStyle(istyle);
|
||||||
@@ -124,11 +128,11 @@ function switchMPRInteractors(api, istyle) {
|
|||||||
// TODO: Not sure why this is required the second time this function is called
|
// TODO: Not sure why this is required the second time this function is called
|
||||||
istyle.setInteractor(renderWindow.getInteractor());
|
istyle.setInteractor(renderWindow.getInteractor());
|
||||||
|
|
||||||
if (istyle.getVolumeMapper() !== api.volumes[0]) {
|
if (currentViewport) {
|
||||||
if (currentNormal) {
|
istyle.setViewport(currentViewport);
|
||||||
istyle.setSliceNormal(currentNormal);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (istyle.getVolumeMapper() !== api.volumes[0]) {
|
||||||
if (currentSlabThickness) {
|
if (currentSlabThickness) {
|
||||||
istyle.setSlabThickness(currentSlabThickness);
|
istyle.setSlabThickness(currentSlabThickness);
|
||||||
}
|
}
|
||||||
@@ -161,7 +165,7 @@ const actions = {
|
|||||||
},
|
},
|
||||||
enableRotateTool: () => {
|
enableRotateTool: () => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const istyle = vtkInteractorStyleMPRSlice.newInstance();
|
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||||
|
|
||||||
switchMPRInteractors(api, istyle);
|
switchMPRInteractors(api, istyle);
|
||||||
});
|
});
|
||||||
@@ -182,7 +186,7 @@ const actions = {
|
|||||||
switchMPRInteractors(api, istyle);
|
switchMPRInteractors(api, istyle);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setSlabThickness: slabThickness => {
|
setSlabThickness: ({ slabThickness }) => {
|
||||||
currentSlabThickness = slabThickness;
|
currentSlabThickness = slabThickness;
|
||||||
|
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
@@ -191,15 +195,6 @@ const actions = {
|
|||||||
|
|
||||||
if (istyle.setSlabThickness) {
|
if (istyle.setSlabThickness) {
|
||||||
istyle.setSlabThickness(currentSlabThickness);
|
istyle.setSlabThickness(currentSlabThickness);
|
||||||
|
|
||||||
// TODO: Do this inside the interactors in a setSlabThickness function instead
|
|
||||||
const renderer = api.genericRenderWindow.getRenderer();
|
|
||||||
const camera = renderer.getActiveCamera();
|
|
||||||
const dist = camera.getDistance();
|
|
||||||
const near = dist - currentSlabThickness / 2;
|
|
||||||
const far = dist + currentSlabThickness / 2;
|
|
||||||
|
|
||||||
camera.setClippingRange(near, far);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderWindow.render();
|
renderWindow.render();
|
||||||
@@ -220,6 +215,32 @@ const actions = {
|
|||||||
renderWindow.render();
|
renderWindow.render();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setBlendModeToComposite: () => {
|
||||||
|
apis.forEach(api => {
|
||||||
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
|
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||||
|
|
||||||
|
const mapper = api.volumes[0].getMapper();
|
||||||
|
if (mapper.setBlendModeToComposite) {
|
||||||
|
mapper.setBlendModeToComposite();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (istyle.setSlabThickness) {
|
||||||
|
istyle.setSlabThickness(defaultSlabThickness);
|
||||||
|
}
|
||||||
|
renderWindow.render();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setBlendModeToMaximumIntensity: () => {
|
||||||
|
apis.forEach(api => {
|
||||||
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
|
const mapper = api.volumes[0].getMapper();
|
||||||
|
if (mapper.setBlendModeToMaximumIntensity) {
|
||||||
|
mapper.setBlendModeToMaximumIntensity();
|
||||||
|
}
|
||||||
|
renderWindow.render();
|
||||||
|
});
|
||||||
|
},
|
||||||
setBlendMode: ({ blendMode }) => {
|
setBlendMode: ({ blendMode }) => {
|
||||||
apis.forEach(api => {
|
apis.forEach(api => {
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
@@ -243,16 +264,19 @@ const actions = {
|
|||||||
|
|
||||||
apis = apiByViewport;
|
apis = apiByViewport;
|
||||||
|
|
||||||
/*const rgbTransferFunction = apiByViewport[0].volumes[0]
|
const rgbTransferFunction = apiByViewport[0].volumes[0]
|
||||||
.getProperty()
|
.getProperty()
|
||||||
.getRGBTransferFunction(0);
|
.getRGBTransferFunction(0);
|
||||||
rgbTransferFunction.onModified(() => {
|
|
||||||
|
const onModifiedSubscription = rgbTransferFunction.onModified(() => {
|
||||||
apiByViewport.forEach(a => {
|
apiByViewport.forEach(a => {
|
||||||
const renderWindow = a.genericRenderWindow.getRenderWindow();
|
const renderWindow = a.genericRenderWindow.getRenderWindow();
|
||||||
|
|
||||||
renderWindow.render();
|
renderWindow.render();
|
||||||
});
|
});
|
||||||
});*/
|
});
|
||||||
|
|
||||||
|
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
|
||||||
|
|
||||||
apiByViewport.forEach((api, index) => {
|
apiByViewport.forEach((api, index) => {
|
||||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||||
@@ -284,18 +308,18 @@ const actions = {
|
|||||||
case 0:
|
case 0:
|
||||||
//Axial
|
//Axial
|
||||||
istyle.setSliceNormal(0, 0, 1);
|
istyle.setSliceNormal(0, 0, 1);
|
||||||
camera.setViewUp(0, -1, 0);
|
istyle.setViewUp(0, -1, 0);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
// sagittal
|
// sagittal
|
||||||
istyle.setSliceNormal(1, 0, 0);
|
istyle.setSliceNormal(1, 0, 0);
|
||||||
camera.setViewUp(0, 0, 1);
|
istyle.setViewUp(0, 0, 1);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
// Coronal
|
// Coronal
|
||||||
istyle.setSliceNormal(0, 1, 0);
|
istyle.setSliceNormal(0, 1, 0);
|
||||||
camera.setViewUp(0, 0, 1);
|
istyle.setViewUp(0, 0, 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,12 +362,12 @@ const definitions = {
|
|||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
setBlendModeToComposite: {
|
setBlendModeToComposite: {
|
||||||
commandFn: actions.setBlendMode,
|
commandFn: actions.setBlendModeToComposite,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||||
},
|
},
|
||||||
setBlendModeToMaximumIntensity: {
|
setBlendModeToMaximumIntensity: {
|
||||||
commandFn: actions.setBlendMode,
|
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Range, Checkbox, Select } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './slab-thickness-toolbar-button.styl';
|
||||||
|
|
||||||
|
const SLIDER = {
|
||||||
|
MIN: 0.1,
|
||||||
|
MAX: 1000,
|
||||||
|
STEP: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarLabel = props => {
|
||||||
|
const { label } = props;
|
||||||
|
return <div className="toolbar-button-label">{label}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarSlider = props => {
|
||||||
|
const { value, min, max, onChange } = props;
|
||||||
|
return (
|
||||||
|
<div className="toolbar-slider-container">
|
||||||
|
<label htmlFor="toolbar-slider">{value}mm</label>
|
||||||
|
<Range
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={SLIDER.STEP}
|
||||||
|
onChange={onChange}
|
||||||
|
id="toolbar-slider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getSelectOptions = button => {
|
||||||
|
return button.operationButtons.map(button => {
|
||||||
|
return {
|
||||||
|
key: button.label,
|
||||||
|
value: button.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getClassNames = (isActive, className) => {
|
||||||
|
return classnames('toolbar-button', 'slab-thickness', className, {
|
||||||
|
active: isActive,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const _applySlabThickness = (
|
||||||
|
value,
|
||||||
|
modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
) => {
|
||||||
|
if (!modeChecked || !toolbarClickCallback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { actionButton } = button;
|
||||||
|
|
||||||
|
const generateOperation = (operation, value) => {
|
||||||
|
// Combine slider value into slider operation
|
||||||
|
const generatedOperation = { ...operation };
|
||||||
|
generatedOperation.commandOptions = {
|
||||||
|
...operation.commandOptions,
|
||||||
|
slabThickness: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
return generatedOperation;
|
||||||
|
};
|
||||||
|
|
||||||
|
const operation = generateOperation(actionButton, value);
|
||||||
|
toolbarClickCallback(operation, event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _applyModeOperation = (
|
||||||
|
operation,
|
||||||
|
modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
) => {
|
||||||
|
// in case modeChecked has not being triggered by user yet
|
||||||
|
if (typeof modeChecked !== 'boolean') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { deactivateButton } = button;
|
||||||
|
|
||||||
|
const _operation = modeChecked ? operation : deactivateButton;
|
||||||
|
if (toolbarClickCallback && _operation) {
|
||||||
|
toolbarClickCallback(_operation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getInitialState = currentSelectedOption => {
|
||||||
|
return {
|
||||||
|
value: SLIDER.MIN,
|
||||||
|
sliderMin: SLIDER.MIN,
|
||||||
|
sliderMax: SLIDER.MAX,
|
||||||
|
modeChecked: undefined,
|
||||||
|
operation: currentSelectedOption,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const INITIAL_OPTION_INDEX = 0;
|
||||||
|
const _getInitialtSelectedOption = (button = {}) => {
|
||||||
|
return (
|
||||||
|
button.operationButtons && button.operationButtons[INITIAL_OPTION_INDEX]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function SlabThicknessToolbarComponent({
|
||||||
|
parentContext,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button,
|
||||||
|
activeButtons,
|
||||||
|
isActive,
|
||||||
|
className,
|
||||||
|
}) {
|
||||||
|
const currentSelectedOption = _getInitialtSelectedOption(button);
|
||||||
|
const [state, setState] = useState(_getInitialState(currentSelectedOption));
|
||||||
|
const { label, operationButtons } = button;
|
||||||
|
const _className = _getClassNames(isActive, className);
|
||||||
|
const selectOptions = _getSelectOptions(button);
|
||||||
|
function onChangeSelect(selectedValue) {
|
||||||
|
// find select value
|
||||||
|
const operation = operationButtons.find(
|
||||||
|
button => button.id === selectedValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (operation === state.operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState({ ...state, operation });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeCheckbox(checked) {
|
||||||
|
setState({ ...state, modeChecked: checked });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangeSlider(event) {
|
||||||
|
const value = Number(event.target.value);
|
||||||
|
|
||||||
|
if (value !== state.value) {
|
||||||
|
setState({ ...state, value, modeChecked: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_applyModeOperation(
|
||||||
|
state.operation,
|
||||||
|
state.modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
);
|
||||||
|
}, [state.modeChecked, state.operation]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_applySlabThickness(
|
||||||
|
state.value,
|
||||||
|
state.modeChecked,
|
||||||
|
toolbarClickCallback,
|
||||||
|
button
|
||||||
|
);
|
||||||
|
}, [state.operation, state.modeChecked, state.value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={_className}>
|
||||||
|
<div className="container">
|
||||||
|
<ToolbarSlider
|
||||||
|
value={state.value}
|
||||||
|
min={state.sliderMin}
|
||||||
|
max={state.sliderMax}
|
||||||
|
onChange={onChangeSlider}
|
||||||
|
/>
|
||||||
|
<ToolbarLabel key="toolbar-label" label={label} />
|
||||||
|
</div>
|
||||||
|
<div className="controller">
|
||||||
|
<Checkbox
|
||||||
|
label="mode"
|
||||||
|
checked={state.modeChecked}
|
||||||
|
onChange={onChangeCheckbox}
|
||||||
|
></Checkbox>
|
||||||
|
<Select
|
||||||
|
key="toolbar-select"
|
||||||
|
options={selectOptions}
|
||||||
|
value={selectOptions[INITIAL_OPTION_INDEX].value}
|
||||||
|
onChange={onChangeSelect}
|
||||||
|
></Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SlabThicknessToolbarComponent.propTypes = {
|
||||||
|
parentContext: PropTypes.object.isRequired,
|
||||||
|
toolbarClickCallback: PropTypes.func.isRequired,
|
||||||
|
button: PropTypes.object.isRequired,
|
||||||
|
activeButtons: PropTypes.array.isRequired,
|
||||||
|
isActive: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SlabThicknessToolbarComponent;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.slab-thickness
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 60px;
|
||||||
|
margin-top: -12px;
|
||||||
|
&:hover
|
||||||
|
color: var(--default-color);
|
||||||
|
.container
|
||||||
|
padding-right: 10px;
|
||||||
|
height: inherit;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
.toolbar-slider-container
|
||||||
|
width: 80px;
|
||||||
|
margin: 0px 2px 2px 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
&:hover
|
||||||
|
color: var(--hover-color);
|
||||||
|
input
|
||||||
|
height: 20px;
|
||||||
|
padding-top: 6px;
|
||||||
|
&::-moz-range-thumb
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -7px;
|
||||||
|
&::-webkit-slider-thumb
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -7px;
|
||||||
|
.controller
|
||||||
|
width: 62px;
|
||||||
|
.ohif-check-container
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding-left: 18px;
|
||||||
|
.ohif-check-label
|
||||||
|
text-transform: capitalize;
|
||||||
|
&:hover
|
||||||
|
color: var(--hover-color);
|
||||||
|
.ohif-checkbox
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: white;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: 22%;
|
||||||
|
&.ohif-checked:after
|
||||||
|
background-color: var(--active-color);
|
||||||
|
top: 17%;
|
||||||
|
left: 17%;
|
||||||
|
.select-ohif
|
||||||
|
color: var(--default-color);
|
||||||
|
border-color: var(--default-color);
|
||||||
|
background-color: black;
|
||||||
|
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
|
||||||
|
&:hover
|
||||||
|
color: var(--hover-color);
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
|
||||||
|
|
||||||
const TOOLBAR_BUTTON_TYPES = {
|
const TOOLBAR_BUTTON_TYPES = {
|
||||||
COMMAND: 'command',
|
COMMAND: 'command',
|
||||||
SET_TOOL_ACTIVE: 'setToolActive',
|
SET_TOOL_ACTIVE: 'setToolActive',
|
||||||
@@ -31,6 +33,7 @@ const definitions = [
|
|||||||
commandName: 'enableRotateTool',
|
commandName: 'enableRotateTool',
|
||||||
commandOptions: {},
|
commandOptions: {},
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
{
|
{
|
||||||
id: 'setBlendModeToComposite',
|
id: 'setBlendModeToComposite',
|
||||||
label: 'Disable MIP',
|
label: 'Disable MIP',
|
||||||
@@ -68,6 +71,50 @@ const definitions = [
|
|||||||
commandName: 'decreaseSlabThickness',
|
commandName: 'decreaseSlabThickness',
|
||||||
commandOptions: {},
|
commandOptions: {},
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
id: 'changeSlabThickness',
|
||||||
|
label: 'Slab Thickness',
|
||||||
|
icon: 'soft-tissue',
|
||||||
|
CustomComponent: SlabThicknessToolbarComponent,
|
||||||
|
commandName: 'setSlabThickness',
|
||||||
|
actionButton: {
|
||||||
|
id: 'setSlabThickness',
|
||||||
|
label: 'slider',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||||
|
commandName: 'setSlabThickness',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
deactivateButton: {
|
||||||
|
id: 'setBlendModeToComposite',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToComposite',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
operationButtons: [
|
||||||
|
{
|
||||||
|
id: 'setBlendModeToMaximumIntensity',
|
||||||
|
label: 'MIP',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToMaximumIntensity',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'setBlendModeToMinimumIntensity',
|
||||||
|
label: 'MinIP',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToMinimumIntensity',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'setBlendModeToAverageIntensity',
|
||||||
|
label: 'AvgIP',
|
||||||
|
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||||
|
commandName: 'setBlendModeToAverageIntensity',
|
||||||
|
commandOptions: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const subscriptions = [];
|
||||||
|
|
||||||
|
// This is pretty hacky right now, but it makes sure we don't keep adding subscriptions.
|
||||||
|
// TODO -> Nuke this and move it up a layer once we have more vigorous layout support.
|
||||||
|
|
||||||
|
const vtkViewportSubscriptionManager = {
|
||||||
|
subscriptions,
|
||||||
|
pushSubscription(viewportIndex, subscription) {
|
||||||
|
if (!Array.isArray(subscriptions[viewportIndex])) {
|
||||||
|
subscriptions[viewportIndex] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions[viewportIndex].push(subscription);
|
||||||
|
},
|
||||||
|
unsubscribe(viewportIndex) {
|
||||||
|
if (!subscriptions[viewportIndex]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (subscriptions[viewportIndex].length) {
|
||||||
|
subscriptions[viewportIndex].pop().unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions[viewportIndex] = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default vtkViewportSubscriptionManager;
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"dev:project": ".scripts/dev.sh",
|
"dev:project": ".scripts/dev.sh",
|
||||||
"dev:orthanc": "lerna run dev:orthanc --stream",
|
"dev:orthanc": "lerna run dev:orthanc --stream",
|
||||||
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
||||||
|
"start": "yarn run dev",
|
||||||
"test": "yarn run test:unit",
|
"test": "yarn run test:unit",
|
||||||
"test:unit": "jest --collectCoverage",
|
"test:unit": "jest --collectCoverage",
|
||||||
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
|
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
|
||||||
|
|||||||
@@ -3,6 +3,44 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [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))
|
||||||
|
|
||||||
|
|
||||||
|
### 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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
|
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "1.0.1",
|
"version": "1.2.0",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import user from '../user';
|
|||||||
* @export
|
* @export
|
||||||
* @param {Object} [server={}]
|
* @param {Object} [server={}]
|
||||||
* @param {Object} [server.requestOptions]
|
* @param {Object} [server.requestOptions]
|
||||||
* @param {string} [server.requestOptions.auth]
|
* @param {string|function} [server.requestOptions.auth]
|
||||||
* @returns {Object} { Authorization }
|
* @returns {Object} { Authorization }
|
||||||
*/
|
*/
|
||||||
export default function getAuthorizationHeader({ requestOptions } = {}) {
|
export default function getAuthorizationHeader({ requestOptions } = {}) {
|
||||||
@@ -19,8 +19,13 @@ export default function getAuthorizationHeader({ requestOptions } = {}) {
|
|||||||
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
||||||
|
|
||||||
if (requestOptions && requestOptions.auth) {
|
if (requestOptions && requestOptions.auth) {
|
||||||
// HTTP Basic Auth (user:password)
|
if (typeof requestOptions.auth === 'function') {
|
||||||
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
// Custom Auth Header
|
||||||
|
headers.Authorization = requestOptions.auth(requestOptions);
|
||||||
|
} else {
|
||||||
|
// HTTP Basic Auth (user:password)
|
||||||
|
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
||||||
|
}
|
||||||
} else if (accessToken) {
|
} else if (accessToken) {
|
||||||
headers.Authorization = `Bearer ${accessToken}`;
|
headers.Authorization = `Bearer ${accessToken}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ describe('getAuthorizationHeader', () => {
|
|||||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
|
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
|
||||||
const validServer = {
|
const validServer = {
|
||||||
requestOptions: {
|
requestOptions: {
|
||||||
auth: {
|
auth: 'dummy_user:dummy_password',
|
||||||
user: 'dummy_user',
|
|
||||||
password: 'dummy_password',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,9 +23,7 @@ describe('getAuthorizationHeader', () => {
|
|||||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => {
|
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no password', () => {
|
||||||
const validServerWithoutPassword = {
|
const validServerWithoutPassword = {
|
||||||
requestOptions: {
|
requestOptions: {
|
||||||
auth: {
|
auth: 'dummy_user',
|
||||||
user: 'dummy_user',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,22 +38,19 @@ describe('getAuthorizationHeader', () => {
|
|||||||
expect(authentication).toEqual(expectedAuthorizationHeader);
|
expect(authentication).toEqual(expectedAuthorizationHeader);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no username', () => {
|
it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => {
|
||||||
const validServerWithoutPassword = {
|
const validServerCustomAuth = {
|
||||||
requestOptions: {
|
requestOptions: {
|
||||||
auth: {
|
auth: options => `Basic ${options.token}`,
|
||||||
user: 'dummy_user',
|
token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==',
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedAuthorizationHeader = {
|
const expectedAuthorizationHeader = {
|
||||||
Authorization: `Basic ${btoa(
|
Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`,
|
||||||
validServerWithoutPassword.requestOptions.auth
|
|
||||||
)}`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const authentication = getAuthorizationHeader(validServerWithoutPassword);
|
const authentication = getAuthorizationHeader(validServerCustomAuth);
|
||||||
|
|
||||||
expect(authentication).toEqual(expectedAuthorizationHeader);
|
expect(authentication).toEqual(expectedAuthorizationHeader);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// import { api } from 'dicomweb-client'
|
// import { api } from 'dicomweb-client'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
DICOMwebClient: jest.fn().mockImplementation(() => {
|
DICOMwebClient: jest.fn().mockImplementation(function() {
|
||||||
return {
|
this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
|
||||||
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
|
this.retrieveSeriesMetadata = jest.fn(function(options) {
|
||||||
};
|
const { studyInstanceUID, seriesInstanceUID } = options;
|
||||||
|
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { SeriesMetadata } from './SeriesMetadata';
|
|||||||
// - createStacks
|
// - createStacks
|
||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
// - createStacks
|
// - createStacks
|
||||||
import { isImage } from './../../utils/isImage';
|
import { isImage } from '../../utils/isImage';
|
||||||
|
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||||
|
|
||||||
export class StudyMetadata extends Metadata {
|
export class StudyMetadata extends Metadata {
|
||||||
constructor(data, uid) {
|
constructor(data, uid) {
|
||||||
@@ -88,6 +89,100 @@ export class StudyMetadata extends Metadata {
|
|||||||
return this._displaySets.slice();
|
return this._displaySets.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a series metadata object into display sets
|
||||||
|
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
||||||
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
|
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
|
||||||
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
|
*/
|
||||||
|
_createDisplaySetsForSeries(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series,
|
||||||
|
givenDisplaySets
|
||||||
|
) {
|
||||||
|
const study = this;
|
||||||
|
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
|
||||||
|
const anyInstances = series.getInstanceCount() > 0;
|
||||||
|
|
||||||
|
if (!anyInstances) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopClassUids = getSopClassUids(series);
|
||||||
|
|
||||||
|
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
|
||||||
|
const displaySet = _getDisplaySetFromSopClassModule(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
sopClassUids
|
||||||
|
);
|
||||||
|
if (displaySet) {
|
||||||
|
displaySet.sopClassModule = true;
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
||||||
|
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
||||||
|
|
||||||
|
// Search through the instances (InstanceMetadata object) of this series
|
||||||
|
// Split Multi-frame instances and Single-image modalities
|
||||||
|
// into their own specific display sets. Place the rest of each
|
||||||
|
// series into another display set.
|
||||||
|
const stackableInstances = [];
|
||||||
|
series.forEachInstance(instance => {
|
||||||
|
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
|
||||||
|
if (
|
||||||
|
!isImage(instance.getRawValue('x00080016')) &&
|
||||||
|
!instance.getRawValue('x00280010')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let displaySet;
|
||||||
|
|
||||||
|
if (isMultiFrame(instance)) {
|
||||||
|
displaySet = makeDisplaySet(series, [instance]);
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
isClip: true,
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
|
||||||
|
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
|
||||||
|
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
||||||
|
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
} else if (isSingleImageModality(instance.modality)) {
|
||||||
|
displaySet = makeDisplaySet(series, [instance]);
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
|
||||||
|
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||||
|
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
||||||
|
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
} else {
|
||||||
|
stackableInstances.push(instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stackableInstances.length) {
|
||||||
|
const displaySet = makeDisplaySet(series, stackableInstances);
|
||||||
|
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
|
||||||
|
displaySet.setAttributes({
|
||||||
|
sopClassUids,
|
||||||
|
});
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return displaySets;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a set of series to be placed in the Study Metadata
|
* Creates a set of series to be placed in the Study Metadata
|
||||||
* The series that appear in the Study Metadata must represent
|
* The series that appear in the Study Metadata must represent
|
||||||
@@ -101,104 +196,44 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @returns {Array} An array of series to be placed in the Study Metadata
|
* @returns {Array} An array of series to be placed in the Study Metadata
|
||||||
*/
|
*/
|
||||||
createDisplaySets(sopClassHandlerModules) {
|
createDisplaySets(sopClassHandlerModules) {
|
||||||
const study = this;
|
|
||||||
const displaySets = [];
|
const displaySets = [];
|
||||||
const anyDisplaySets = study.getSeriesCount();
|
const anyDisplaySets = this.getSeriesCount();
|
||||||
const anySopClassHandlerModules =
|
|
||||||
sopClassHandlerModules && sopClassHandlerModules.length > 0;
|
|
||||||
|
|
||||||
if (!anyDisplaySets) {
|
if (!anyDisplaySets) {
|
||||||
return displaySets;
|
return displaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through the series (SeriesMetadata)
|
// Loop through the series (SeriesMetadata)
|
||||||
this.forEachSeries(series => {
|
this.forEachSeries(
|
||||||
const anyInstances = series.getInstanceCount() > 0;
|
series =>
|
||||||
if (!anyInstances) {
|
void this._createDisplaySetsForSeries(
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopClassUids = getSopClassUids(series);
|
|
||||||
|
|
||||||
if (anySopClassHandlerModules) {
|
|
||||||
const displaySet = _getDisplaySetFromSopClassModule(
|
|
||||||
sopClassHandlerModules,
|
sopClassHandlerModules,
|
||||||
series,
|
series,
|
||||||
study,
|
displaySets
|
||||||
sopClassUids
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displaySet) {
|
return sortDisplaySetList(displaySets);
|
||||||
displaySet.sopClassModule = true;
|
}
|
||||||
displaySets.push(displaySet);
|
|
||||||
|
|
||||||
return;
|
sortDisplaySets() {
|
||||||
}
|
sortDisplaySetList(this._displaySets);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
|
/**
|
||||||
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
|
* Method to append display sets from a given series to the internal list of display sets
|
||||||
|
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
|
||||||
// Search through the instances (InstanceMetadata object) of this series
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
// Split Multi-frame instances and Single-image modalities
|
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
||||||
// into their own specific display sets. Place the rest of each
|
*/
|
||||||
// series into another display set.
|
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||||
const stackableInstances = [];
|
if (this.containsSeries(series)) {
|
||||||
series.forEachInstance(instance => {
|
this.setDisplaySets(
|
||||||
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
|
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||||
if (
|
);
|
||||||
!isImage(instance.getRawValue('x00080016')) &&
|
return true;
|
||||||
!instance.getRawValue('x00280010')
|
}
|
||||||
) {
|
return false;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let displaySet;
|
|
||||||
|
|
||||||
if (isMultiFrame(instance)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
isClip: true,
|
|
||||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
|
||||||
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
|
|
||||||
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
|
|
||||||
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
|
||||||
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else if (isSingleImageModality(instance.modality)) {
|
|
||||||
displaySet = makeDisplaySet(series, [instance]);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
|
|
||||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
|
||||||
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
|
|
||||||
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
} else {
|
|
||||||
stackableInstances.push(instance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (stackableInstances.length) {
|
|
||||||
const displaySet = makeDisplaySet(series, stackableInstances);
|
|
||||||
displaySet.setAttribute(
|
|
||||||
'studyInstanceUid',
|
|
||||||
study.getStudyInstanceUID()
|
|
||||||
);
|
|
||||||
displaySet.setAttributes({
|
|
||||||
sopClassUids,
|
|
||||||
});
|
|
||||||
displaySets.push(displaySet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
displaySets.sort(_sortBySeriesNumber);
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,7 +241,10 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @param {Array} displaySets Array of display sets (ImageSet[])
|
* @param {Array} displaySets Array of display sets (ImageSet[])
|
||||||
*/
|
*/
|
||||||
setDisplaySets(displaySets) {
|
setDisplaySets(displaySets) {
|
||||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
||||||
|
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||||
|
this.sortDisplaySets();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,6 +358,12 @@ export class StudyMetadata extends Metadata {
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
containsSeries(series) {
|
||||||
|
return (
|
||||||
|
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the number of series within the current study.
|
* Retrieve the number of series within the current study.
|
||||||
* @returns {number} The number of series in the current study.
|
* @returns {number} The number of series in the current study.
|
||||||
@@ -650,13 +694,51 @@ function _getDisplaySetFromSopClassModule(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Sort series primarily by modality (i.e., series with references to other
|
||||||
|
* series like SEG, KO or PR are grouped in the end of the list) and then by
|
||||||
|
* series number:
|
||||||
|
*
|
||||||
|
* --------
|
||||||
|
* | CT #3 |
|
||||||
|
* | CT #4 |
|
||||||
|
* | CT #5 |
|
||||||
|
* --------
|
||||||
|
* | SEG #1 |
|
||||||
|
* | SEG #2 |
|
||||||
|
* --------
|
||||||
*
|
*
|
||||||
* @param {*} a - DisplaySet
|
* @param {*} a - DisplaySet
|
||||||
* @param {*} b - DisplaySet
|
* @param {*} b - DisplaySet
|
||||||
*/
|
*/
|
||||||
function _sortBySeriesNumber(a, b) {
|
|
||||||
|
function seriesSortingCriteria(a, b) {
|
||||||
|
const isLowPriorityA = isLowPriorityModality(a.modality);
|
||||||
|
const isLowPriorityB = isLowPriorityModality(b.modality);
|
||||||
|
if (!isLowPriorityA && isLowPriorityB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (isLowPriorityA && !isLowPriorityB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return sortBySeriesNumber(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort series by series number. Series with low
|
||||||
|
* @param {*} a - DisplaySet
|
||||||
|
* @param {*} b - DisplaySet
|
||||||
|
*/
|
||||||
|
function sortBySeriesNumber(a, b) {
|
||||||
const seriesNumberAIsGreaterOrUndefined =
|
const seriesNumberAIsGreaterOrUndefined =
|
||||||
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
||||||
|
|
||||||
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
|
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts a list of display set objects
|
||||||
|
* @param {Array} list A list of display sets to be sorted
|
||||||
|
*/
|
||||||
|
function sortDisplaySetList(list) {
|
||||||
|
return list.sort(seriesSortingCriteria);
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
import DICOMWeb from '../../../DICOMWeb/';
|
import DICOMWeb from '../../../DICOMWeb/';
|
||||||
|
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
|
||||||
|
|
||||||
|
const INFO = Symbol('INFO');
|
||||||
|
|
||||||
const WADOProxy = {
|
const WADOProxy = {
|
||||||
convertURL: (url, server) => {
|
convertURL: (url, server) => {
|
||||||
@@ -94,9 +97,7 @@ function buildInstanceWadoRsUri(
|
|||||||
seriesInstanceUid,
|
seriesInstanceUid,
|
||||||
sopInstanceUid
|
sopInstanceUid
|
||||||
) {
|
) {
|
||||||
return `${
|
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||||
server.wadoRoot
|
|
||||||
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInstanceFrameWadoRsUri(
|
function buildInstanceFrameWadoRsUri(
|
||||||
@@ -166,9 +167,9 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const arrayBufferToPaletteColorLUT = result => {
|
const arrayBufferToPaletteColorLUT = result => {
|
||||||
const arraybuffer = result[0]
|
const arraybuffer = result[0];
|
||||||
const byteArray = new Uint8Array(arraybuffer)
|
const byteArray = new Uint8Array(arraybuffer);
|
||||||
const lut = []
|
const lut = [];
|
||||||
|
|
||||||
for (let i = 0; i < numLutEntries; i++) {
|
for (let i = 0; i < numLutEntries; i++) {
|
||||||
if (bits === 16) {
|
if (bits === 16) {
|
||||||
@@ -271,200 +272,338 @@ function getRadiopharmaceuticalInfo(instance) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function makeSOPInstance(server, study, instance) {
|
||||||
|
const { studyInstanceUid } = study;
|
||||||
|
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||||
|
let series = study.seriesMap[seriesInstanceUid];
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
series = {
|
||||||
|
seriesInstanceUid,
|
||||||
|
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||||
|
modality: DICOMWeb.getString(instance['00080060']),
|
||||||
|
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||||
|
seriesDate: DICOMWeb.getString(instance['00080021']),
|
||||||
|
seriesTime: DICOMWeb.getString(instance['00080031']),
|
||||||
|
instances: [],
|
||||||
|
};
|
||||||
|
study.seriesMap[seriesInstanceUid] = series;
|
||||||
|
study.seriesList.push(series);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||||
|
const wadouri = buildInstanceWadoUrl(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
const baseWadoRsUri = buildInstanceWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
const wadorsuri = buildInstanceFrameWadoRsUri(
|
||||||
|
server,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUid,
|
||||||
|
sopInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
const sopInstance = {
|
||||||
|
imageType: DICOMWeb.getString(instance['00080008']),
|
||||||
|
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||||
|
modality: DICOMWeb.getString(instance['00080060']),
|
||||||
|
sopInstanceUid,
|
||||||
|
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||||
|
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||||
|
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||||
|
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||||
|
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||||
|
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||||
|
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||||
|
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
||||||
|
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||||
|
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||||
|
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||||
|
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
||||||
|
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||||
|
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||||
|
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||||
|
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||||
|
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
||||||
|
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
||||||
|
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||||
|
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||||
|
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||||
|
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||||
|
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
||||||
|
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||||
|
laterality: DICOMWeb.getString(instance['00200062']),
|
||||||
|
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||||
|
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
||||||
|
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||||
|
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
||||||
|
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||||
|
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
|
||||||
|
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
||||||
|
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
||||||
|
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
||||||
|
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
||||||
|
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
||||||
|
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
||||||
|
echoNumber: DICOMWeb.getString(instance['00180086']),
|
||||||
|
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
||||||
|
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
||||||
|
baseWadoRsUri: baseWadoRsUri,
|
||||||
|
wadouri: WADOProxy.convertURL(wadouri, server),
|
||||||
|
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
||||||
|
wadoRoot: server.wadoRoot,
|
||||||
|
imageRendering: server.imageRendering,
|
||||||
|
thumbnailRendering: server.thumbnailRendering,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
||||||
|
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
|
||||||
|
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281101'])
|
||||||
|
);
|
||||||
|
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281102'])
|
||||||
|
);
|
||||||
|
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
||||||
|
DICOMWeb.getString(instance['00281103'])
|
||||||
|
);
|
||||||
|
const palettes = await getPaletteColors(
|
||||||
|
server,
|
||||||
|
instance,
|
||||||
|
redPaletteColorLookupTableDescriptor
|
||||||
|
);
|
||||||
|
|
||||||
|
if (palettes) {
|
||||||
|
if (palettes.uid) {
|
||||||
|
sopInstance.paletteColorLookupTableUID = palettes.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
sopInstance.redPaletteColorLookupTableData = palettes.red;
|
||||||
|
sopInstance.greenPaletteColorLookupTableData = palettes.green;
|
||||||
|
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
|
||||||
|
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
||||||
|
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
||||||
|
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
series.instances.push(sopInstance);
|
||||||
|
return sopInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a plain JS object that describes a study (a study descriptor object)
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {Object} aSopInstance a SOP Instance from which study information will be added
|
||||||
|
*/
|
||||||
|
function createStudy(server, aSopInstance) {
|
||||||
|
// TODO: Pass a reference ID to the server instead of including the URLs here
|
||||||
|
return {
|
||||||
|
seriesList: [],
|
||||||
|
seriesMap: Object.create(null),
|
||||||
|
seriesLoader: null,
|
||||||
|
wadoUriRoot: server.wadoUriRoot,
|
||||||
|
wadoRoot: server.wadoRoot,
|
||||||
|
qidoRoot: server.qidoRoot,
|
||||||
|
patientName: DICOMWeb.getName(aSopInstance['00100010']),
|
||||||
|
patientId: DICOMWeb.getString(aSopInstance['00100020']),
|
||||||
|
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
|
||||||
|
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
|
||||||
|
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
|
||||||
|
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
|
||||||
|
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
|
||||||
|
modalities: DICOMWeb.getString(aSopInstance['00080061']),
|
||||||
|
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
|
||||||
|
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
|
||||||
|
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
|
||||||
|
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a list of SOP Instances to a given study object descriptor
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {Object} study The study descriptor to which the given SOP instances will be added
|
||||||
|
* @param {Array} sopInstanceList A list of SOP instance objects
|
||||||
|
*/
|
||||||
|
async function addInstancesToStudy(server, study, sopInstanceList) {
|
||||||
|
return Promise.all(
|
||||||
|
sopInstanceList.map(function(sopInstance) {
|
||||||
|
return makeSOPInstance(server, study, sopInstance);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses result data from a WADO search into Study MetaData
|
* Parses result data from a WADO search into Study MetaData
|
||||||
* Returns an object populated with study metadata, including the
|
* Returns an object populated with study metadata, including the
|
||||||
* series list.
|
* series list.
|
||||||
*
|
*
|
||||||
* @param server
|
* @param {Object} server Object with server configuration paramenters
|
||||||
* @param studyInstanceUid
|
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
|
||||||
* @param resultData
|
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
|
||||||
*/
|
*/
|
||||||
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
|
||||||
if (!resultData.length) {
|
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
|
||||||
return;
|
const firstSopInstance = sopInstanceList[0];
|
||||||
|
const study = createStudy(server, firstSopInstance);
|
||||||
|
await addInstancesToStudy(server, study, sopInstanceList);
|
||||||
|
return study;
|
||||||
}
|
}
|
||||||
|
throw new Error('Failed to create study out of provided SOP instance list');
|
||||||
const anInstance = resultData[0];
|
|
||||||
if (!anInstance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Pass a reference ID to the server instead of including the URLs here
|
|
||||||
const studyData = {
|
|
||||||
seriesList: [],
|
|
||||||
wadoUriRoot: server.wadoUriRoot,
|
|
||||||
wadoRoot: server.wadoRoot,
|
|
||||||
qidoRoot: server.qidoRoot,
|
|
||||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
|
||||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
|
||||||
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
|
|
||||||
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
|
|
||||||
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
|
|
||||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
|
||||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
|
||||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
|
||||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
|
||||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
|
||||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
|
||||||
institutionName: DICOMWeb.getString(anInstance['00080080']),
|
|
||||||
};
|
|
||||||
|
|
||||||
const seriesMap = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
resultData.map(async function(instance) {
|
|
||||||
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
|
||||||
let series = seriesMap[seriesInstanceUid];
|
|
||||||
|
|
||||||
if (!series) {
|
|
||||||
series = {
|
|
||||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
|
||||||
modality: DICOMWeb.getString(instance['00080060']),
|
|
||||||
seriesInstanceUid: seriesInstanceUid,
|
|
||||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
|
||||||
seriesDate: DICOMWeb.getString(instance['00080021']),
|
|
||||||
seriesTime: DICOMWeb.getString(instance['00080031']),
|
|
||||||
instances: [],
|
|
||||||
};
|
|
||||||
seriesMap[seriesInstanceUid] = series;
|
|
||||||
studyData.seriesList.push(series);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
|
||||||
const wadouri = buildInstanceWadoUrl(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
const baseWadoRsUri = buildInstanceWadoRsUri(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
const wadorsuri = buildInstanceFrameWadoRsUri(
|
|
||||||
server,
|
|
||||||
studyInstanceUid,
|
|
||||||
seriesInstanceUid,
|
|
||||||
sopInstanceUid
|
|
||||||
);
|
|
||||||
|
|
||||||
const instanceSummary = {
|
|
||||||
imageType: DICOMWeb.getString(instance['00080008']),
|
|
||||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
|
||||||
modality: DICOMWeb.getString(instance['00080060']),
|
|
||||||
sopInstanceUid,
|
|
||||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
|
||||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
|
||||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
|
||||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
|
||||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
|
||||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
|
||||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
|
||||||
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
|
||||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
|
||||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
|
||||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
|
||||||
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
|
||||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
|
||||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
|
||||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
|
||||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
|
||||||
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
|
||||||
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
|
||||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
|
||||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
|
||||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
|
||||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
|
||||||
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
|
||||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
|
||||||
laterality: DICOMWeb.getString(instance['00200062']),
|
|
||||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
|
||||||
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
|
||||||
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
|
||||||
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
|
||||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
|
||||||
frameTimeVector: parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00181065'])
|
|
||||||
),
|
|
||||||
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
|
||||||
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
|
||||||
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
|
||||||
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
|
||||||
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
|
||||||
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
|
||||||
echoNumber: DICOMWeb.getString(instance['00180086']),
|
|
||||||
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
|
||||||
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
|
||||||
baseWadoRsUri: baseWadoRsUri,
|
|
||||||
wadouri: WADOProxy.convertURL(wadouri, server),
|
|
||||||
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
|
||||||
wadoRoot: server.wadoRoot,
|
|
||||||
imageRendering: server.imageRendering,
|
|
||||||
thumbnailRendering: server.thumbnailRendering,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
|
||||||
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
|
|
||||||
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281101'])
|
|
||||||
);
|
|
||||||
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281102'])
|
|
||||||
);
|
|
||||||
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
|
||||||
DICOMWeb.getString(instance['00281103'])
|
|
||||||
);
|
|
||||||
const palettes = await getPaletteColors(
|
|
||||||
server,
|
|
||||||
instance,
|
|
||||||
redPaletteColorLookupTableDescriptor
|
|
||||||
);
|
|
||||||
|
|
||||||
if (palettes) {
|
|
||||||
if (palettes.uid) {
|
|
||||||
instanceSummary.paletteColorLookupTableUID = palettes.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
instanceSummary.redPaletteColorLookupTableData = palettes.red;
|
|
||||||
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
|
|
||||||
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
|
|
||||||
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
|
||||||
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
|
||||||
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
series.instances.push(instanceSummary);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return studyData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve Study MetaData from a DICOM server using a WADO call
|
* Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
|
||||||
*
|
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed
|
||||||
* @param server
|
* @param {Object} server Object with server configuration paramenters
|
||||||
* @param studyInstanceUid
|
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
|
||||||
* @returns {Promise}
|
* @returns {Object} A study descriptor object
|
||||||
*/
|
*/
|
||||||
async function RetrieveMetadata(server, studyInstanceUid) {
|
async function RetrieveMetadata(server, studyInstanceUid) {
|
||||||
const config = {
|
return (server.enableStudyLazyLoad !== false
|
||||||
|
? lazyLoadStudyMetadata
|
||||||
|
: loadStudyMetadata)(server, studyInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} server
|
||||||
|
* @param {*} studyInstanceUID
|
||||||
|
*/
|
||||||
|
async function loadStudyMetadata(server, studyInstanceUID) {
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
url: server.wadoRoot,
|
url: server.wadoRoot,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
};
|
});
|
||||||
const dicomWeb = new api.DICOMwebClient(config);
|
return dicomWeb
|
||||||
const options = {
|
.retrieveStudyMetadata({ studyInstanceUID })
|
||||||
studyInstanceUID: studyInstanceUid,
|
.then(result => createStudyFromSOPInstanceList(server, result));
|
||||||
};
|
}
|
||||||
|
|
||||||
return dicomWeb.retrieveStudyMetadata(options).then(result => {
|
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
|
||||||
return resultDataToStudyMetadata(server, studyInstanceUid, result);
|
const seriesInstanceUids = await searchStudySeries(server, studyInstanceUid);
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
|
url: server.wadoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
const seriesLoader = makeSeriesLoader(
|
||||||
|
dicomWeb,
|
||||||
|
studyInstanceUid,
|
||||||
|
seriesInstanceUids
|
||||||
|
);
|
||||||
|
const firstSeries = await seriesLoader.next();
|
||||||
|
const study = await createStudyFromSOPInstanceList(
|
||||||
|
server,
|
||||||
|
firstSeries.sopInstances
|
||||||
|
);
|
||||||
|
if (seriesLoader.hasNext()) {
|
||||||
|
attachSeriesLoader(server, study, seriesLoader);
|
||||||
|
}
|
||||||
|
return study;
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachSeriesLoader(server, study, seriesLoader) {
|
||||||
|
study.seriesLoader = Object.freeze({
|
||||||
|
hasNext() {
|
||||||
|
return seriesLoader.hasNext();
|
||||||
|
},
|
||||||
|
async next() {
|
||||||
|
const series = await seriesLoader.next();
|
||||||
|
await addInstancesToStudy(server, study, series.sopInstances);
|
||||||
|
return study.seriesMap[series.seriesInstanceUID];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
||||||
|
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||||
|
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
|
||||||
|
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
|
||||||
|
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
|
||||||
|
*/
|
||||||
|
function makeSeriesLoader(
|
||||||
|
dicomWebClient,
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUIDList
|
||||||
|
) {
|
||||||
|
return Object.freeze({
|
||||||
|
hasNext() {
|
||||||
|
return seriesInstanceUIDList.length > 0;
|
||||||
|
},
|
||||||
|
async next() {
|
||||||
|
const seriesInstanceUID = seriesInstanceUIDList.shift();
|
||||||
|
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
|
||||||
|
studyInstanceUID,
|
||||||
|
seriesInstanceUID,
|
||||||
|
});
|
||||||
|
return { studyInstanceUID, seriesInstanceUID, sopInstances };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search series of a given study
|
||||||
|
* @param {Object} server Object with server configuration paramenters
|
||||||
|
* @param {string} studyInstanceUID The Study Instance UID to search series from;
|
||||||
|
* @returns {Arrays} A list of Series Instance UIDs
|
||||||
|
*/
|
||||||
|
async function searchStudySeries(server, studyInstanceUID) {
|
||||||
|
const dicomWeb = new api.DICOMwebClient({
|
||||||
|
url: server.qidoRoot,
|
||||||
|
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||||
|
});
|
||||||
|
const seriesList = await dicomWeb.searchForSeries({ studyInstanceUID });
|
||||||
|
return seriesList
|
||||||
|
.sort(seriesSortingCriteria)
|
||||||
|
.map(series => getSeriesInfo(series).seriesInstanceUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Series sorting criteria: series considered low priority are moved to the end
|
||||||
|
* of the list and series number is used to break ties
|
||||||
|
* @param {Object} firstSeries
|
||||||
|
* @param {Object} secondSeries
|
||||||
|
*/
|
||||||
|
function seriesSortingCriteria(firstSeries, secondSeries) {
|
||||||
|
const a = getSeriesInfo(firstSeries);
|
||||||
|
const b = getSeriesInfo(secondSeries);
|
||||||
|
if (!a.isLowPriority && b.isLowPriority) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.isLowPriority && !b.isLowPriority) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.seriesNumber - b.seriesNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an object with processed series information and saves its reference
|
||||||
|
* inside the series object itself to simplify sorting
|
||||||
|
* @param {Object} series The raw series object
|
||||||
|
*/
|
||||||
|
function getSeriesInfo(series) {
|
||||||
|
let info = series[INFO];
|
||||||
|
if (!info) {
|
||||||
|
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
|
||||||
|
info = Object.freeze({
|
||||||
|
modality,
|
||||||
|
isLowPriority: isLowPriorityModality(modality),
|
||||||
|
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
|
||||||
|
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
|
||||||
|
});
|
||||||
|
series[INFO] = info;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
export default RetrieveMetadata;
|
export default RetrieveMetadata;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR']);
|
||||||
|
|
||||||
|
export default function isLowPriorityModality(modality) {
|
||||||
|
return LOW_PRIORITY_MODALITIES.includes(modality);
|
||||||
|
}
|
||||||
@@ -70,32 +70,48 @@ const getWadoRsInstanceMetaData = (study, series, instance) => {
|
|||||||
.toJSON();
|
.toJSON();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function updateMetaDataManager(study) {
|
/**
|
||||||
study.seriesList.forEach(series => {
|
* Update metadata manager with instances of a specifc series
|
||||||
series.instances.forEach(instance => {
|
* @param {Object} study A plain study descriptor object
|
||||||
// Cache just images that are going to be loaded via WADO-RS
|
* @param {Object} series A Series descriptor object contaning the instances to be added to the manager
|
||||||
if (
|
*/
|
||||||
instance.imageRendering !== 'wadors' &&
|
function updateMetaDataManagerForSeries(study, series) {
|
||||||
instance.thumbnailRendering !== 'wadors'
|
series.instances.forEach(instance => {
|
||||||
) {
|
// Cache just images that are going to be loaded via WADO-RS
|
||||||
return;
|
if (
|
||||||
}
|
instance.imageRendering !== 'wadors' &&
|
||||||
|
instance.thumbnailRendering !== 'wadors'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
||||||
const numberOfFrames = instance.numberOfFrames || 1;
|
const numberOfFrames = instance.numberOfFrames || 1;
|
||||||
|
|
||||||
// We can share the same metaData with all frames because it doesn't have
|
// We can share the same metaData with all frames because it doesn't have
|
||||||
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
||||||
// WADO-RS frame number is 1-based
|
// WADO-RS frame number is 1-based
|
||||||
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
||||||
const imageId = getWADORSImageId(instance, frameNumber);
|
const imageId = getWADORSImageId(instance, frameNumber);
|
||||||
|
|
||||||
// TODO Drop dependency on this
|
// TODO Drop dependency on this
|
||||||
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
|
cornerstoneWADOImageLoader.wadors.metaDataManager.add(imageId, metaData);
|
||||||
imageId,
|
}
|
||||||
metaData
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update metadata manager
|
||||||
|
* @param {Object} study A plain study descriptor object
|
||||||
|
* @param {string} [seriesInstanceUid] The Series Instance UID of the series to be added (Optional)
|
||||||
|
*/
|
||||||
|
export default function updateMetaDataManager(study, seriesInstanceUid) {
|
||||||
|
if (seriesInstanceUid) {
|
||||||
|
const series = study.seriesMap[seriesInstanceUid];
|
||||||
|
updateMetaDataManagerForSeries(study, series);
|
||||||
|
} else {
|
||||||
|
study.seriesList.forEach(series => {
|
||||||
|
updateMetaDataManagerForSeries(study, series);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.4...@ohif/i18n@0.50.5) (2019-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* CineDialog buttons label ([#998](https://github.com/OHIF/Viewers/issues/998)) ([4df624b](https://github.com/OHIF/Viewers/commit/4df624b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.3...@ohif/i18n@0.50.4) (2019-09-10)
|
## [0.50.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.3...@ohif/i18n@0.50.4) (2019-09-10)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/i18n
|
**Note:** Version bump only for package @ohif/i18n
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "0.50.4",
|
"version": "0.50.5",
|
||||||
"description": "Internationalization library for The OHIF Viewer",
|
"description": "Internationalization library for The OHIF Viewer",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
"Skip to first image": "Skip to first $t(Common:Image)",
|
"Skip to first image": "Skip to first $t(Common:Image)",
|
||||||
"Skip to last image": "Skip, to last $t(Common:Image)",
|
"Skip to last image": "Skip to last $t(Common:Image)",
|
||||||
"fps": "fps"
|
"fps": "fps"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Next image": " $t(Common:Image) $t(Common:Next)",
|
"Next image": "$t(Common:Image) $t(Common:Next)",
|
||||||
"Play / Stop": "$t(Common:Play) / Stop",
|
"Play / Stop": "$t(Common:Play) / Stop",
|
||||||
"Previous image": "$t(Common:Image) $t(Common:Previous)",
|
"Previous image": "$t(Common:Image) $t(Common:Previous)",
|
||||||
"Skip to first image": "Ir a la primera $t(Common:Image)",
|
"Skip to first image": "Ir a la primera $t(Common:Image)",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
"Skip to first image": "$t(Common:Image)最初にスキップ",
|
"Skip to first image": "$t(Common:Image)最初にスキップ",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
"Skip to first image": "Bỏ qua đến đầu $t(Common:Image)",
|
"Skip to first image": "Bỏ qua đến đầu $t(Common:Image)",
|
||||||
|
|||||||
@@ -3,6 +3,50 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.0...@ohif/ui@0.53.1) (2019-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Move Series Information to Separate Row ([#990](https://github.com/OHIF/Viewers/issues/990)) ([458d310](https://github.com/OHIF/Viewers/commit/458d310))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.52.0...@ohif/ui@0.53.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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.3...@ohif/ui@0.52.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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.2...@ohif/ui@0.51.3) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Address issues with touch devices and drag/drop causing crashes ([#982](https://github.com/OHIF/Viewers/issues/982)) ([cf40a83](https://github.com/OHIF/Viewers/commit/cf40a83))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.1...@ohif/ui@0.51.2) (2019-09-26)
|
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.1...@ohif/ui@0.51.2) (2019-09-26)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "0.51.2",
|
"version": "0.53.1",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -6,15 +6,29 @@ export class Checkbox extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
checked: PropTypes.bool,
|
checked: PropTypes.bool,
|
||||||
|
onChange: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { checked: props.checked, label: props.label };
|
this.state = { checked: !!props.checked, label: props.label };
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(e) {
|
handleChange(e) {
|
||||||
this.setState({ checked: e.target.checked });
|
const checked = e.target.checked;
|
||||||
|
this.setState({ checked });
|
||||||
|
if (this.props.onChange) this.props.onChange(checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(props) {
|
||||||
|
const { checked = false, label } = props;
|
||||||
|
|
||||||
|
if (this.state.checked !== checked || this.state.label !== label) {
|
||||||
|
this.setState({
|
||||||
|
checked,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -32,9 +46,7 @@ export class Checkbox extends Component {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={this.state.checked}
|
checked={this.state.checked}
|
||||||
onChange={e => {
|
onChange={this.handleChange.bind(this)}
|
||||||
this.handleChange(e);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{checkbox}
|
{checkbox}
|
||||||
{this.state.label}
|
{this.state.label}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
} from './userPreferencesModal';
|
} from './userPreferencesModal';
|
||||||
|
|
||||||
|
import { Checkbox } from './checkbox';
|
||||||
import { CineDialog } from './cineDialog';
|
import { CineDialog } from './cineDialog';
|
||||||
import { QuickSwitch } from './quickSwitch';
|
import { QuickSwitch } from './quickSwitch';
|
||||||
import { RoundedButtonGroup } from './roundedButtonGroup';
|
import { RoundedButtonGroup } from './roundedButtonGroup';
|
||||||
@@ -23,6 +24,7 @@ import { ToolbarSection } from './toolbarSection';
|
|||||||
import { Tooltip } from './tooltip';
|
import { Tooltip } from './tooltip';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
Checkbox,
|
||||||
CineDialog,
|
CineDialog,
|
||||||
ExampleDropTarget,
|
ExampleDropTarget,
|
||||||
LayoutButton,
|
LayoutButton,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* global cornerstone */
|
||||||
import './ImageThumbnail.styl';
|
import './ImageThumbnail.styl';
|
||||||
|
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
@@ -12,25 +13,6 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
|
|||||||
// - Pass in the entire load and render function as a prop?
|
// - Pass in the entire load and render function as a prop?
|
||||||
//import cornerstone from 'cornerstone-core';
|
//import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous wrapper around Cornerstone's renderToCanvas method.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} canvasElement An HTML <canvas> element
|
|
||||||
* @param {Image} image A Cornerstone Image
|
|
||||||
*
|
|
||||||
* @return {Promise} A promise tracking the progress of the rendering. Resolves empty.
|
|
||||||
*/
|
|
||||||
function renderAsync(canvasElement, image) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
cornerstone.renderToCanvas(canvasElement, image);
|
|
||||||
resolve();
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ImageThumbnail extends PureComponent {
|
export default class ImageThumbnail extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
imageSrc: PropTypes.string,
|
imageSrc: PropTypes.string,
|
||||||
@@ -52,40 +34,32 @@ export default class ImageThumbnail extends PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.canvas = React.createRef();
|
this.canvas = React.createRef();
|
||||||
|
|
||||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: renderIntoCanvas,
|
loading: this.shouldRenderToCanvas(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldRenderToCanvas() {
|
||||||
|
return this.props.imageId && !this.props.imageSrc;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
if (this.shouldRenderToCanvas()) {
|
||||||
|
cornerstone
|
||||||
if (renderIntoCanvas) {
|
.loadAndCacheImage(this.props.imageId)
|
||||||
const { imageId } = this.props;
|
.then(image => {
|
||||||
const canvas = this.canvas.current;
|
cornerstone.renderToCanvas(this.canvas.current, image);
|
||||||
|
this.setState({
|
||||||
cornerstone.loadAndCacheImage(imageId).then(
|
loading: false,
|
||||||
image => {
|
});
|
||||||
renderAsync(canvas, image).then(
|
})
|
||||||
() => {
|
.catch(error => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
error: true,
|
||||||
},
|
});
|
||||||
error => {
|
|
||||||
// TODO: Set state?
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
// TODO: Set state?
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +74,10 @@ export default class ImageThumbnail extends PureComponent {
|
|||||||
const showStackLoadingProgressBar =
|
const showStackLoadingProgressBar =
|
||||||
this.props.stackPercentComplete !== undefined;
|
this.props.stackPercentComplete !== undefined;
|
||||||
|
|
||||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ImageThumbnail">
|
<div className="ImageThumbnail">
|
||||||
<div className="image-thumbnail-canvas">
|
<div className="image-thumbnail-canvas">
|
||||||
{renderIntoCanvas ? (
|
{this.shouldRenderToCanvas() ? (
|
||||||
<canvas
|
<canvas
|
||||||
ref={this.canvas}
|
ref={this.canvas}
|
||||||
width={this.props.width}
|
width={this.props.width}
|
||||||
@@ -130,6 +102,9 @@ export default class ImageThumbnail extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{this.state.loading && (
|
||||||
|
<div className="image-thumbnail-loading-indicator"></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,36 @@
|
|||||||
-webkit-background-clip: padding
|
-webkit-background-clip: padding
|
||||||
background-clip: padding-box
|
background-clip: padding-box
|
||||||
|
|
||||||
|
// Thumbnail Loading Indicator
|
||||||
|
|
||||||
|
.image-thumbnail-loading-indicator
|
||||||
|
position: absolute
|
||||||
|
left: 50%
|
||||||
|
top: 50%
|
||||||
|
margin: -22px 0 0 -22px
|
||||||
|
display: block
|
||||||
|
width: 44px
|
||||||
|
height: 44px
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: " "
|
||||||
|
display: block
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
margin: 1px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 5px solid var(--ui-border-color-dark)
|
||||||
|
border-color: var(--ui-border-color-dark) transparent var(--ui-border-color-dark) transparent
|
||||||
|
animation: image-thumbnail-loading-indicator 1.2s linear infinite
|
||||||
|
|
||||||
|
@keyframes image-thumbnail-loading-indicator
|
||||||
|
0%
|
||||||
|
transform: rotate(0deg)
|
||||||
|
100%
|
||||||
|
transform: rotate(360deg)
|
||||||
|
|
||||||
|
// The End...
|
||||||
|
|
||||||
.image-thumbnail-canvas
|
.image-thumbnail-canvas
|
||||||
height: 100%
|
height: 100%
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
@@ -36,7 +66,6 @@
|
|||||||
-webkit-user-drag: none
|
-webkit-user-drag: none
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|
||||||
|
|
||||||
.thumbnailLoadingIndicator
|
.thumbnailLoadingIndicator
|
||||||
display: none
|
display: none
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class StudyBrowser extends Component {
|
|||||||
if (this.props.supportsDragAndDrop) {
|
if (this.props.supportsDragAndDrop) {
|
||||||
return (
|
return (
|
||||||
<ThumbnailEntryDragSource
|
<ThumbnailEntryDragSource
|
||||||
key={`${studyIndex}_${thumbIndex}`}
|
key={thumb.displaySetInstanceUid}
|
||||||
{...study}
|
{...study}
|
||||||
{...thumb}
|
{...thumb}
|
||||||
id={`${studyIndex}_${thumbIndex}`}
|
id={`${studyIndex}_${thumbIndex}`}
|
||||||
@@ -37,7 +37,7 @@ class StudyBrowser extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className="ThumbnailEntryContainer">
|
<div className="ThumbnailEntryContainer">
|
||||||
<ThumbnailEntry
|
<ThumbnailEntry
|
||||||
key={`${studyIndex}_${thumbIndex}`}
|
key={thumb.displaySetInstanceUid}
|
||||||
{...study}
|
{...study}
|
||||||
{...thumb}
|
{...thumb}
|
||||||
id={`${studyIndex}_${thumbIndex}`}
|
id={`${studyIndex}_${thumbIndex}`}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class ThumbnailEntry extends Component {
|
|||||||
let className = classnames('ThumbnailEntry noselect', {
|
let className = classnames('ThumbnailEntry noselect', {
|
||||||
active: this.props.active,
|
active: this.props.active,
|
||||||
});
|
});
|
||||||
const infoOnly = false;
|
|
||||||
|
|
||||||
let contents = null;
|
let contents = null;
|
||||||
if (this.props.imageSrc || this.props.imageId) {
|
if (this.props.imageSrc || this.props.imageId) {
|
||||||
@@ -54,8 +53,10 @@ class ThumbnailEntry extends Component {
|
|||||||
);
|
);
|
||||||
} else if (this.props.altImageText) {
|
} else if (this.props.altImageText) {
|
||||||
contents = (
|
contents = (
|
||||||
<div className={'alt-image-text p-x-1'}>
|
<div className="p-x-1">
|
||||||
<h1>{this.props.altImageText}</h1>
|
<div className="alt-image-text">
|
||||||
|
<h1>{this.props.altImageText}</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -69,23 +70,23 @@ class ThumbnailEntry extends Component {
|
|||||||
>
|
>
|
||||||
{contents}
|
{contents}
|
||||||
<div
|
<div
|
||||||
className={infoOnly ? 'series-details info-only' : 'series-details'}
|
className="series-details"
|
||||||
>
|
>
|
||||||
<div className="series-description">
|
<div className="series-description">
|
||||||
{this.props.seriesDescription}
|
{this.props.seriesDescription}
|
||||||
</div>
|
</div>
|
||||||
<div className="series-information">
|
<div className="series-information">
|
||||||
<div className="item item-series clearfix">
|
<div className="item item-series">
|
||||||
<div className="icon">S:</div>
|
<div className="icon">S:</div>
|
||||||
<div className="value">{this.props.seriesNumber}</div>
|
<div className="value">{this.props.seriesNumber}</div>
|
||||||
</div>
|
</div>
|
||||||
{hasInstanceNumber && (
|
{hasInstanceNumber && (
|
||||||
<div className="item item-series clearfix">
|
<div className="item item-series">
|
||||||
<div className="icon">I:</div>
|
<div className="icon">I:</div>
|
||||||
<div className="value">{this.props.instanceNumber}</div>
|
<div className="value">{this.props.instanceNumber}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="item item-frames clearfix">
|
<div className="item item-frames">
|
||||||
<div className="icon">
|
<div className="icon">
|
||||||
<div />
|
<div />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,8 +31,6 @@
|
|||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
|
|
||||||
.series-details
|
.series-details
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
font-size: 14px
|
font-size: 14px
|
||||||
line-height: 1.3em
|
line-height: 1.3em
|
||||||
@@ -44,9 +42,6 @@
|
|||||||
position: relative
|
position: relative
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
|
|
||||||
.series-description
|
|
||||||
flex-grow: 1
|
|
||||||
|
|
||||||
&.info-only
|
&.info-only
|
||||||
.series-description
|
.series-description
|
||||||
display: none
|
display: none
|
||||||
@@ -67,36 +62,33 @@
|
|||||||
float: none
|
float: none
|
||||||
line-height: 25px
|
line-height: 25px
|
||||||
|
|
||||||
.value
|
|
||||||
margin-left: 0
|
|
||||||
width: auto
|
|
||||||
|
|
||||||
.series-information
|
.series-information
|
||||||
padding-right: 4px
|
display: flex
|
||||||
max-width: 50px
|
flex-direction: row
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
.item
|
||||||
|
flex: 1
|
||||||
|
text-align: center
|
||||||
|
|
||||||
.item-frames .icon
|
.item-frames .icon
|
||||||
height: 18px
|
height: 18px
|
||||||
|
|
||||||
.value
|
.value
|
||||||
color: var(--text-secondary-color);
|
color: var(--text-secondary-color);
|
||||||
display: inline-block
|
display: inline
|
||||||
float: right
|
|
||||||
font-size: 12px
|
font-size: 12px
|
||||||
margin-left: 4px
|
margin-left: 4px
|
||||||
overflow: hidden
|
|
||||||
text-overflow: ellipsis
|
text-overflow: ellipsis
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
width: calc(100% - 15px)
|
|
||||||
|
|
||||||
.icon
|
.icon
|
||||||
color: var(--active-color);
|
color: var(--active-color);
|
||||||
display: inline-block
|
display: inline
|
||||||
float: left
|
|
||||||
font-size: 10px
|
font-size: 10px
|
||||||
font-weight: 900
|
font-weight: 900
|
||||||
text-align: right
|
text-align: right
|
||||||
width: 11px
|
|
||||||
|
|
||||||
div
|
div
|
||||||
background-color: var(--series-count-background-color);
|
background-color: var(--series-count-background-color);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.range:focus {
|
.range:focus {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class Range extends React.Component {
|
|||||||
|
|
||||||
handleChange = event => {
|
handleChange = event => {
|
||||||
this.setState({ value: event.target.value });
|
this.setState({ value: event.target.value });
|
||||||
if (this.props.onChange) this.props.onChange();
|
if (this.props.onChange) this.props.onChange(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -21,6 +21,7 @@ class Range extends React.Component {
|
|||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
min={this.props.min}
|
min={this.props.min}
|
||||||
max={this.props.max}
|
max={this.props.max}
|
||||||
|
step={this.props.step || 1}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
id={this.props.id}
|
id={this.props.id}
|
||||||
className="range"
|
className="range"
|
||||||
|
|||||||
@@ -34,10 +34,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.select-ohif:focus {
|
.select-ohif:focus {
|
||||||
border-color: #aaa;
|
|
||||||
box-shadow: 0 0 1px 3px rgba(59, 153, 252, 0.7);
|
|
||||||
box-shadow: 0 0 0 3px -moz-mac-focusring;
|
|
||||||
color: #222;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ class Select extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleChange = event => {
|
handleChange = event => {
|
||||||
this.setState({ value: event.target.value });
|
const value = event.target.value;
|
||||||
if (this.props.onChange) this.props.onChange();
|
this.setState({ value });
|
||||||
|
if (this.props.onChange) this.props.onChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
Checkbox,
|
||||||
CineDialog,
|
CineDialog,
|
||||||
ExampleDropTarget,
|
ExampleDropTarget,
|
||||||
LayoutButton,
|
LayoutButton,
|
||||||
@@ -25,7 +26,7 @@ import {
|
|||||||
import { ICONS, Icon } from './elements';
|
import { ICONS, Icon } from './elements';
|
||||||
|
|
||||||
// Alias this for now as not all dependents are using strict versioning
|
// Alias this for now as not all dependents are using strict versioning
|
||||||
import { DropdownMenu as Dropdown } from './elements/form';
|
import { DropdownMenu as Dropdown, Range, Select } from './elements/form';
|
||||||
import ExpandableToolMenu from './viewer/ExpandableToolMenu.js';
|
import ExpandableToolMenu from './viewer/ExpandableToolMenu.js';
|
||||||
import LayoutManager from './LayoutChooser/LayoutManager.js';
|
import LayoutManager from './LayoutChooser/LayoutManager.js';
|
||||||
import LayoutPanelDropTarget from './LayoutChooser/LayoutPanelDropTarget.js';
|
import LayoutPanelDropTarget from './LayoutChooser/LayoutPanelDropTarget.js';
|
||||||
@@ -38,6 +39,7 @@ import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
|
|||||||
export {
|
export {
|
||||||
ICONS,
|
ICONS,
|
||||||
//
|
//
|
||||||
|
Checkbox,
|
||||||
CineDialog,
|
CineDialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
ExpandableToolMenu,
|
ExpandableToolMenu,
|
||||||
@@ -53,8 +55,10 @@ export {
|
|||||||
OverlayTrigger,
|
OverlayTrigger,
|
||||||
PlayClipButton,
|
PlayClipButton,
|
||||||
QuickSwitch,
|
QuickSwitch,
|
||||||
|
Range,
|
||||||
RoundedButtonGroup,
|
RoundedButtonGroup,
|
||||||
ScrollableArea,
|
ScrollableArea,
|
||||||
|
Select,
|
||||||
SelectTree,
|
SelectTree,
|
||||||
SimpleDialog,
|
SimpleDialog,
|
||||||
StudyBrowser,
|
StudyBrowser,
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ const isTouchDevice = !!('ontouchstart' in window || navigator.maxTouchPoints);
|
|||||||
//
|
//
|
||||||
// http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context
|
// http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context
|
||||||
export default function viewerbaseDragDropContext(DecoratedClass) {
|
export default function viewerbaseDragDropContext(DecoratedClass) {
|
||||||
const backend = isTouchDevice ? TouchBackend({ enableMouseEvents: true }) : HTML5Backend;
|
const backend = isTouchDevice ? TouchBackend : HTML5Backend;
|
||||||
|
const opts = isTouchDevice ? { enableMouseEvents: true } : {};
|
||||||
|
|
||||||
return (props) => (
|
return (props) => (
|
||||||
<DndProvider backend={backend}>
|
<DndProvider backend={backend} opts={opts}>
|
||||||
<DecoratedClass {...props}/>
|
<DecoratedClass {...props}/>
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -108,16 +108,8 @@ class ExpandableToolMenu extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
const getToolBarButtonComponent = () => {
|
||||||
<OverlayTrigger
|
return (
|
||||||
key="menu-button"
|
|
||||||
trigger="click"
|
|
||||||
placement="bottom"
|
|
||||||
rootClose={true}
|
|
||||||
handleHide={this.onOverlayHide}
|
|
||||||
onClick={this.onExpandableToolClick}
|
|
||||||
overlay={this.toolbarMenuOverlay()}
|
|
||||||
>
|
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
key="menu-button"
|
key="menu-button"
|
||||||
type="tool"
|
type="tool"
|
||||||
@@ -128,6 +120,22 @@ class ExpandableToolMenu extends React.Component {
|
|||||||
isExpandable={true}
|
isExpandable={true}
|
||||||
isExpanded={this.state.isExpanded}
|
isExpanded={this.state.isExpanded}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toolbarComponent = getToolBarButtonComponent();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
key="menu-button"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom"
|
||||||
|
rootClose={true}
|
||||||
|
handleHide={this.onOverlayHide}
|
||||||
|
onClick={this.onExpandableToolClick}
|
||||||
|
overlay={this.toolbarMenuOverlay()}
|
||||||
|
>
|
||||||
|
{toolbarComponent}
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ module.exports = (env, argv) => {
|
|||||||
hot: true,
|
hot: true,
|
||||||
open: true,
|
open: true,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
public: 'http://localhost:' + 3000,
|
||||||
historyApiFallback: {
|
historyApiFallback: {
|
||||||
disableDotRule: true,
|
disableDotRule: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,111 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.4.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.4...@ohif/viewer@1.4.5) (2019-10-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.3...@ohif/viewer@1.4.4) (2019-10-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.2...@ohif/viewer@1.4.3) (2019-10-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.1...@ohif/viewer@1.4.2) (2019-10-04)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.0...@ohif/viewer@1.4.1) (2019-10-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.3...@ohif/viewer@1.4.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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.2...@ohif/viewer@1.3.3) (2019-10-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.1...@ohif/viewer@1.3.2) (2019-10-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.0...@ohif/viewer@1.3.1) (2019-10-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Exit MPR mode if Layout is changed ([#984](https://github.com/OHIF/Viewers/issues/984)) ([674ca9f](https://github.com/OHIF/Viewers/commit/674ca9f))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.8...@ohif/viewer@1.3.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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.7...@ohif/viewer@1.2.8) (2019-10-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.6...@ohif/viewer@1.2.7) (2019-10-01)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.5...@ohif/viewer@1.2.6) (2019-09-27)
|
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.5...@ohif/viewer@1.2.6) (2019-09-27)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/viewer
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
describe('OHIF Cornerstone Toolbar', () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.openStudy("MISTER^MR");
|
||||||
|
cy.waitDicomImage();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
//Creating aliases for Cornerstone tools buttons
|
||||||
|
cy.get('.toolbar-button:nth-child(2)').as('stackScrollBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(3)').as('zoomBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(4)').as('levelsBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(5)').as('panBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(6)').as('lengthBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(7)').as('annotateBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(8)').as('angleBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(9)').as('resetBtn');
|
||||||
|
cy.get('.toolbar-button:nth-child(10)').as('cineBtn');
|
||||||
|
cy.get('.expandableToolMenu').as('moreBtn');
|
||||||
|
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
|
||||||
|
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
|
||||||
|
cy.get('.pull-right > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton').as('measurementsBtn');
|
||||||
|
cy.get('.viewport-element').as('viewport');
|
||||||
|
cy.get('section.sidepanel.from-right').as('measurementsPanel')
|
||||||
|
//Following best practices, reset should be done before each test
|
||||||
|
cy.resetViewport();
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if all primary buttons are being displayed', () => {
|
||||||
|
cy.get('@stackScrollBtn').should('be.visible').contains('Stack Scroll');
|
||||||
|
cy.get('@zoomBtn').should('be.visible').contains('Zoom');
|
||||||
|
cy.get('@levelsBtn').should('be.visible').contains('Levels');
|
||||||
|
cy.get('@panBtn').should('be.visible').contains('Pan');
|
||||||
|
cy.get('@lengthBtn').should('be.visible').contains('Length');
|
||||||
|
cy.get('@annotateBtn').should('be.visible').contains('Annotate');
|
||||||
|
cy.get('@angleBtn').should('be.visible').contains('Angle');
|
||||||
|
cy.get('@resetBtn').should('be.visible').contains('Reset');
|
||||||
|
cy.get('@cineBtn').should('be.visible').contains('CINE');
|
||||||
|
cy.get('@moreBtn').should('be.visible').contains('More');
|
||||||
|
cy.get('@twodmprBtn').should('be.visible').contains('2D MPR');
|
||||||
|
cy.get('@layoutBtn').should('be.visible').contains('Layout');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Stack Scroll tool will navigate across all series in the viewport', () => {
|
||||||
|
//Click on button and vefiry if icon is active on toolbar
|
||||||
|
cy.get('@stackScrollBtn')
|
||||||
|
.click()
|
||||||
|
.then(($stackScrollBtn) =>{
|
||||||
|
cy.wrap($stackScrollBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
//drags the mouse inside the viewport to be able to interact with series
|
||||||
|
cy.get('@viewport')
|
||||||
|
.trigger('mousedown', 'top', { which: 1 })
|
||||||
|
.trigger('mousemove', 'center', { which: 1 })
|
||||||
|
.trigger('mouseup');
|
||||||
|
|
||||||
|
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-left.overlay-element > div';
|
||||||
|
const expectedText = 'Ser: 1Img: 14 14/26256 x 256Loc: 0.00 mm Thick: 5.00 mm';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('have.text', expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Zoom tool will zoom in/out an image in the viewport', () => {
|
||||||
|
//Click on button and vefiry if icon is active on toolbar
|
||||||
|
cy.get('@zoomBtn')
|
||||||
|
.click()
|
||||||
|
.then(($zoomBtn) =>{
|
||||||
|
cy.wrap($zoomBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
//drags the mouse inside the viewport to be able to interact with series
|
||||||
|
cy.get('@viewport')
|
||||||
|
.trigger('mousedown', 'top', { which: 1 })
|
||||||
|
.trigger('mousemove', 'center', { which: 1 })
|
||||||
|
.trigger('mouseup');
|
||||||
|
|
||||||
|
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-right.overlay-element > div';
|
||||||
|
const expectedText = 'Zoom: 884%W: 820 L: 410Lossless / Uncompressed';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('have.text', expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Levels tool will change the contrast and brightness of an image in the viewport', () => {
|
||||||
|
//Click on button and vefiry if icon is active on toolbar
|
||||||
|
cy.get('@levelsBtn')
|
||||||
|
.click()
|
||||||
|
.then(($levelsBtn) =>{
|
||||||
|
cy.wrap($levelsBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
//drags the mouse inside the viewport to be able to interact with series
|
||||||
|
cy.get('@viewport')
|
||||||
|
.trigger('mousedown', 'top', { which: 1 })
|
||||||
|
.trigger('mousemove', 'center', { which: 1 })
|
||||||
|
.trigger('mouseup')
|
||||||
|
.trigger('mousedown', 'center', { which: 1 })
|
||||||
|
.trigger('mousemove', 'left', { which: 1 })
|
||||||
|
.trigger('mouseup');
|
||||||
|
|
||||||
|
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-right.overlay-element > div';
|
||||||
|
const expectedText = 'Zoom: 211%W: 544 L: 626Lossless / Uncompressed';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('have.text', expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Pan tool will move the image inside the viewport', () => {
|
||||||
|
//Click on button and vefiry if icon is active on toolbar
|
||||||
|
cy.get('@panBtn')
|
||||||
|
.click()
|
||||||
|
.then(($panBtn) =>{
|
||||||
|
cy.wrap($panBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
//Get image position from cornerstone and check if y axis was modified
|
||||||
|
let cornerstone;
|
||||||
|
let currentPan;
|
||||||
|
|
||||||
|
cy.window()
|
||||||
|
.its('cornerstone')
|
||||||
|
.then((c) => {
|
||||||
|
cornerstone = c;
|
||||||
|
currentPan = () => cornerstone.getEnabledElements()[0].viewport.translation;
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('@viewport')
|
||||||
|
.trigger('mousedown', 'center', { which: 1 })
|
||||||
|
.trigger('mousemove', 'bottom', { which: 1 })
|
||||||
|
.trigger('mouseup', 'bottom')
|
||||||
|
.then(() => {
|
||||||
|
expect(currentPan().y > 0).to.eq(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Length annotation can be added on viewport and on measurements panel', () => {
|
||||||
|
//Click on button and vefiry if icon is active on toolbar
|
||||||
|
cy.get('@lengthBtn')
|
||||||
|
.click()
|
||||||
|
.then(($lengthbtn) =>{
|
||||||
|
cy.wrap($lengthbtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
//Add annotation on the viewport
|
||||||
|
const firstClick = [150, 100];
|
||||||
|
const secondClick = [130, 170];
|
||||||
|
cy.addLine('.cornerstone-canvas', firstClick, secondClick)
|
||||||
|
|
||||||
|
//Verify if measurement annotation was added into the measurements panel
|
||||||
|
cy.get('@measurementsBtn')
|
||||||
|
.click()
|
||||||
|
.then($measurementsBtn => {
|
||||||
|
cy.get('@measurementsPanel')
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get('.measurementItem')
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 1);
|
||||||
|
|
||||||
|
cy.wrap($measurementsBtn)
|
||||||
|
.click();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Angle annotation can be added on viewport and on measurements panel', () => {
|
||||||
|
//Click on button and vefiry if icon is active on toolbar
|
||||||
|
cy.get('@angleBtn')
|
||||||
|
.click()
|
||||||
|
.then(($angleBtn) =>{
|
||||||
|
cy.wrap($angleBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
//Add annotation on the viewport
|
||||||
|
const initPos = [180, 390];
|
||||||
|
const midPos = [300, 410];
|
||||||
|
const finalPos = [180, 450];
|
||||||
|
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
|
||||||
|
|
||||||
|
//Verify if measurement annotation was added into the measurements panel
|
||||||
|
cy.get('@measurementsBtn')
|
||||||
|
.click()
|
||||||
|
.then($measurementsBtn => {
|
||||||
|
cy.get('@measurementsPanel')
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
cy.get('.measurementItem')
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 2);
|
||||||
|
|
||||||
|
cy.wrap($measurementsBtn)
|
||||||
|
.click();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Reset tool will reset all changes made on the image', () => {
|
||||||
|
//Make some changes by zooming in and rotating the image
|
||||||
|
cy.imageZoomIn();
|
||||||
|
cy.imageContrast();
|
||||||
|
|
||||||
|
//Click on reset button
|
||||||
|
cy.get('@resetBtn').click()
|
||||||
|
|
||||||
|
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-right.overlay-element > div';
|
||||||
|
const expectedText = 'Zoom: 211%W: 820 L: 410Lossless / Uncompressed';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('have.text', expectedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if CINE tool will prompt a modal with working controls', () => {
|
||||||
|
//Click on button
|
||||||
|
cy.get('@cineBtn')
|
||||||
|
.click();
|
||||||
|
//Vefiry if cine control overlay is being displayed
|
||||||
|
cy.get('.cine-controls').as('cineControls')
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
//Test PLAY button
|
||||||
|
cy.get('[title="Play / Stop"]')
|
||||||
|
.click()
|
||||||
|
.wait(100)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-left.overlay-element > div';
|
||||||
|
let expectedText = 'Img: 1 1/26';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('not.have.text', expectedText);
|
||||||
|
|
||||||
|
//Test SKIP TO FIRST IMAGE button
|
||||||
|
cy.get('[title="Skip to first Image"]')
|
||||||
|
.click()
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('contain.text', expectedText);
|
||||||
|
|
||||||
|
//Test NEXT IMAGE button
|
||||||
|
cy.get('[title="Next Image"]') //Title is wrong and was reported on bug #995: https://github.com/OHIF/Viewers/issues/995
|
||||||
|
.click()
|
||||||
|
expectedText = 'Img: 2 2/26';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('contain.text', expectedText);
|
||||||
|
|
||||||
|
//Test SKIP TO LAST IMAGE button
|
||||||
|
cy.get('[title="Skip to last Image"]') //Title is wrong and was reported on bug #995: https://github.com/OHIF/Viewers/issues/995
|
||||||
|
.click()
|
||||||
|
expectedText = 'Img: 27 26/26';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('contain.text', expectedText);
|
||||||
|
|
||||||
|
//Test PREVIOUS IMAGE button
|
||||||
|
cy.get('[title="Previous Image"]')
|
||||||
|
.click()
|
||||||
|
expectedText = 'Img: 26 25/26';
|
||||||
|
cy.get(overlaySeriesInformation)
|
||||||
|
.should('contain.text', expectedText);
|
||||||
|
|
||||||
|
//Click on Cine button
|
||||||
|
cy.get('@cineBtn')
|
||||||
|
.click();
|
||||||
|
//Vefiry if cine control overlay is hidden
|
||||||
|
cy.get('@cineControls')
|
||||||
|
.should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if More button will prompt a modal with secondary tools', () => {
|
||||||
|
cy.get('@moreBtn')
|
||||||
|
.click();
|
||||||
|
//Verify if overlay is displayed
|
||||||
|
cy.get('.tooltip-toolbar-overlay').as('toolbarOverlay')
|
||||||
|
.should('be.visible');
|
||||||
|
|
||||||
|
let iconName;
|
||||||
|
//Click on one of the secondary tools from the overlay
|
||||||
|
cy.get('.tooltip-inner > :nth-child(1)')
|
||||||
|
.click()
|
||||||
|
.then(($magnifyBtn) =>{
|
||||||
|
cy.wrap($magnifyBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
.find('svg').then(($icon)=>{
|
||||||
|
iconName = $icon.text();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
//Check if More button is active and if it has same icon as the secondary tool selected
|
||||||
|
cy.get('@moreBtn')
|
||||||
|
.click()
|
||||||
|
.then(($moreBtn) =>{
|
||||||
|
cy.wrap($moreBtn)
|
||||||
|
.should('have.class', 'active')
|
||||||
|
.contains(iconName);
|
||||||
|
})
|
||||||
|
|
||||||
|
//Verify if overlay is hidden
|
||||||
|
cy.get('@toolbarOverlay')
|
||||||
|
.should('not.be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('checks if Layout tool will multiply the number of viewports displayed', () => {
|
||||||
|
//Click on Layout button and verify if overlay is displayed
|
||||||
|
cy.get('@layoutBtn')
|
||||||
|
.click()
|
||||||
|
.then(() => {
|
||||||
|
cy.get('.layoutChooser').as('layoutChooser')
|
||||||
|
.should('be.visible')
|
||||||
|
.find('td')
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 9);
|
||||||
|
})
|
||||||
|
|
||||||
|
//verify if layout has changed to 2 viewports
|
||||||
|
cy.get('tbody > :nth-child(1) > :nth-child(2)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 2);
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(2) > :nth-child(1)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 2);
|
||||||
|
})
|
||||||
|
|
||||||
|
//verify if layout has changed to 3 viewports
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wait(1000);
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 3);
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(3) > :nth-child(1)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 3);
|
||||||
|
})
|
||||||
|
|
||||||
|
//verify if layout has changed to 4 viewports
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(2) > :nth-child(2)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 4);
|
||||||
|
})
|
||||||
|
|
||||||
|
//verify if layout has changed to 6 viewports
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(2) > :nth-child(3)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 6);
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(3) > :nth-child(2)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 6);
|
||||||
|
})
|
||||||
|
|
||||||
|
//verify if layout has changed to 9 viewports
|
||||||
|
cy.get('@layoutBtn').click();
|
||||||
|
cy.get('tbody > :nth-child(3) > :nth-child(3)').click();
|
||||||
|
cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.its('length')
|
||||||
|
.should('be.eq', 9);
|
||||||
|
})
|
||||||
|
|
||||||
|
//Commented this to avoid throwing an wrong exception at the end of "Layout button" test
|
||||||
|
//this commented section should be uncommented once issue #999 is fixed. (https://github.com/OHIF/Viewers/issues/999)
|
||||||
|
|
||||||
|
//verify if layout has changed to 1 viewport
|
||||||
|
// cy.get('@layoutBtn').click();
|
||||||
|
// cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
|
||||||
|
// cy.get('.viewport-container').then(($viewport) =>{
|
||||||
|
// cy.wrap($viewport)
|
||||||
|
// .its('length')
|
||||||
|
// .should('be.eq', 1);
|
||||||
|
// })
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
/*
|
|
||||||
TODO: Temporarily commented out because it is failing on CI due to timing issues
|
|
||||||
describe('OHIF Study Viewer Page', () => {
|
describe('OHIF Study Viewer Page', () => {
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
@@ -41,10 +40,8 @@ describe('OHIF Study Viewer Page', () => {
|
|||||||
cy.get(measurementsButton).click();
|
cy.get(measurementsButton).click();
|
||||||
cy.get(rightPanel).should('be.visible');
|
cy.get(rightPanel).should('be.visible');
|
||||||
|
|
||||||
|
|
||||||
cy.get(measurementsButton).click();
|
cy.get(measurementsButton).click();
|
||||||
cy.get(rightPanel).should('not.be.enabled');
|
cy.get(rightPanel).should('not.be.enabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { DragSimulator } from "../helpers/DragSimulator.js";
|
import { DragSimulator } from "../helpers/DragSimulator.js";
|
||||||
|
import { doesNotReject } from "assert";
|
||||||
|
import { disconnect } from "cluster";
|
||||||
|
|
||||||
// ***********************************************
|
// ***********************************************
|
||||||
// This example commands.js shows you how to
|
// This example commands.js shows you how to
|
||||||
@@ -30,12 +32,114 @@ Cypress.Commands.add('openStudy', (patientName) => {
|
|||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('#patientName')
|
cy.get('#patientName')
|
||||||
.type(patientName);
|
.type(patientName);
|
||||||
cy.get('.studylistStudy > .patientName')
|
|
||||||
.contains(patientName)
|
cy.get('.studylistStudy > .patientName').as('patientResult')
|
||||||
.click();
|
.then({ timeout: 5000 }, ($patientResult) => {
|
||||||
|
cy.contains(patientName)
|
||||||
|
.click();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add('drag', {prevSubject: 'element',},
|
Cypress.Commands.add('drag', {prevSubject: 'element',},
|
||||||
(...args) => DragSimulator.simulate(...args)
|
(...args) => DragSimulator.simulate(...args)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to perform two clicks into two different positions. Each position must be [x, y].
|
||||||
|
* The positions are considering the element as reference, therefore, top-left of the element will be (0, 0).
|
||||||
|
*
|
||||||
|
* @param {*} viewport - Selector for viewport we would like to interact with
|
||||||
|
* @param {number[]} firstClick - Click position [x, y]
|
||||||
|
* @param {number[]} secondClick - Click position [x, y]
|
||||||
|
*/
|
||||||
|
Cypress.Commands.add('addLine', (viewport, firstClick, secondClick) => {
|
||||||
|
|
||||||
|
cy.get(viewport).then(($viewport) => {
|
||||||
|
const [ x1, y1 ] = firstClick;
|
||||||
|
const [ x2, y2 ] = secondClick;
|
||||||
|
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.click(x1, y1, {force: true})
|
||||||
|
.trigger('mousemove', {clientX:x2, clientY:y2})
|
||||||
|
.click(x2, y2, {force: true})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to perform three clicks into three different positions. Each position must be [x, y].
|
||||||
|
* The positions are considering the element as reference, therefore, top-left of the element will be (0, 0).
|
||||||
|
*
|
||||||
|
* @param {*} viewport - Selector for viewport we would like to interact with
|
||||||
|
* @param {number[]} firstClick - Click position [x, y]
|
||||||
|
* @param {number[]} secondClick - Click position [x, y]
|
||||||
|
* @param {number[]} thirdClick - Click position [x, y]
|
||||||
|
*/
|
||||||
|
Cypress.Commands.add('addAngle', (viewport, firstClick, secondClick, thirdClick) => {
|
||||||
|
|
||||||
|
cy.get(viewport).then(($viewport) => {
|
||||||
|
const [ x1, y1 ] = firstClick;
|
||||||
|
const [ x2, y2 ] = secondClick;
|
||||||
|
const [ x3, y3 ] = thirdClick;
|
||||||
|
|
||||||
|
cy.wrap($viewport)
|
||||||
|
.click(x1, y1, {force: true})
|
||||||
|
.trigger('mousemove', {clientX:x2, clientY:y2})
|
||||||
|
.click(x2, y2, {force: true})
|
||||||
|
.trigger('mousemove', {clientX:x3, clientY:y3})
|
||||||
|
.click(x3, y3, {force: true})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//Command to wait DICOM image to load into the viewport
|
||||||
|
Cypress.Commands.add('waitDicomImage', (timeout = 10000) => {
|
||||||
|
cy.window()
|
||||||
|
.its('cornerstone')
|
||||||
|
.then($cornerstone => {
|
||||||
|
return new Cypress.Promise(resolve => {
|
||||||
|
const onEvent = (renderedEvt) => {
|
||||||
|
const element = renderedEvt.detail.element;
|
||||||
|
|
||||||
|
element.removeEventListener('cornerstoneimagerendered', onEvent);
|
||||||
|
$cornerstone.events.removeEventListener('cornerstoneimagerendered', onEvent);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
const onEnabled = (enabledEvt) => {
|
||||||
|
const element = enabledEvt.detail.element;
|
||||||
|
|
||||||
|
element.addEventListener('cornerstoneimagerendered', onEvent);
|
||||||
|
}
|
||||||
|
$cornerstone.events.addEventListener('cornerstoneelementenabled', onEnabled);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//Command to reset the viewport changes throught the cornerstone method
|
||||||
|
Cypress.Commands.add('resetViewport', () => {
|
||||||
|
cy.get('@resetBtn').click()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Cypress.Commands.add('imageZoomIn', () => {
|
||||||
|
cy.get('@zoomBtn').click();
|
||||||
|
|
||||||
|
//drags the mouse inside the viewport to be able to interact with series
|
||||||
|
cy.get('@viewport')
|
||||||
|
.trigger('mousedown', 'top', { which: 1 })
|
||||||
|
.trigger('mousemove', 'center', { which: 1 })
|
||||||
|
.trigger('mouseup');
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add('imageContrast', () => {
|
||||||
|
cy.get('@levelsBtn').click();
|
||||||
|
|
||||||
|
//drags the mouse inside the viewport to be able to interact with series
|
||||||
|
cy.get('@viewport')
|
||||||
|
.trigger('mousedown', 'center', { which: 1 })
|
||||||
|
.trigger('mousemove', 'top', { which: 1 })
|
||||||
|
.trigger('mouseup');
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "1.2.6",
|
"version": "1.4.5",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -46,14 +46,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^1.0.1",
|
"@ohif/core": "^1.2.0",
|
||||||
"@ohif/extension-cornerstone": "^2.0.0",
|
"@ohif/extension-cornerstone": "^2.0.0",
|
||||||
"@ohif/extension-dicom-html": "^1.0.0",
|
"@ohif/extension-dicom-html": "^1.0.0",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||||
"@ohif/extension-vtk": "^0.50.22",
|
"@ohif/extension-vtk": "^0.52.2",
|
||||||
"@ohif/i18n": "^0.50.4",
|
"@ohif/i18n": "^0.50.5",
|
||||||
"@ohif/ui": "^0.51.2",
|
"@ohif/ui": "^0.53.1",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
enableStudyLazyLoad: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
enableStudyLazyLoad: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: true,
|
qidoSupportsIncludeField: true,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
|
enableStudyLazyLoad: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,15 +13,23 @@ const mapStateToProps = state => {
|
|||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
// TODO: Change if layout switched becomes more complex
|
// TODO: Change if layout switched becomes more complex
|
||||||
onChange: selectedCell => {
|
onChange: (selectedCell, currentLayout) => {
|
||||||
let viewports = [];
|
let viewports = [];
|
||||||
const rows = selectedCell.row + 1;
|
const rows = selectedCell.row + 1;
|
||||||
const columns = selectedCell.col + 1;
|
const columns = selectedCell.col + 1;
|
||||||
const numViewports = rows * columns;
|
const numViewports = rows * columns;
|
||||||
for (let i = 0; i < numViewports; i++) {
|
for (let i = 0; i < numViewports; i++) {
|
||||||
|
// Hacky way to allow users to exit MPR "mode"
|
||||||
|
const viewport = currentLayout.viewports[i];
|
||||||
|
let plugin = viewport && viewport.plugin;
|
||||||
|
if (viewport && viewport.vtk) {
|
||||||
|
plugin = 'cornerstone';
|
||||||
|
}
|
||||||
|
|
||||||
viewports.push({
|
viewports.push({
|
||||||
height: `${100 / rows}%`,
|
height: `${100 / rows}%`,
|
||||||
width: `${100 / columns}%`,
|
width: `${100 / columns}%`,
|
||||||
|
plugin,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const layout = {
|
const layout = {
|
||||||
@@ -33,9 +41,19 @@ const mapDispatchToProps = dispatch => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mergeProps = (propsFromState, propsFromDispatch) => {
|
||||||
|
const onChangeFromDispatch = propsFromDispatch.onChange;
|
||||||
|
const { currentLayout } = propsFromState;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onChange: (selectedCell) => onChangeFromDispatch(selectedCell, currentLayout)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const ConnectedLayoutButton = connect(
|
const ConnectedLayoutButton = connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps,
|
||||||
|
mergeProps
|
||||||
)(LayoutButton);
|
)(LayoutButton);
|
||||||
|
|
||||||
export default ConnectedLayoutButton;
|
export default ConnectedLayoutButton;
|
||||||
@@ -229,6 +229,7 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A handy way for us to handle different button types. IE. firing commands for
|
* A handy way for us to handle different button types. IE. firing commands for
|
||||||
* buttons, or initiation built in behavior.
|
* buttons, or initiation built in behavior.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from 'react';
|
||||||
import { metadata, studies, utils } from "@ohif/core";
|
import { metadata, studies, utils, log } from '@ohif/core';
|
||||||
|
|
||||||
import ConnectedViewer from "./ConnectedViewer.js";
|
import ConnectedViewer from './ConnectedViewer.js';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from 'prop-types';
|
||||||
import { extensionManager } from "./../App.js";
|
import { extensionManager } from './../App.js';
|
||||||
|
|
||||||
const { OHIFStudyMetadata } = metadata;
|
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
||||||
const { retrieveStudiesMetadata } = studies;
|
const { retrieveStudiesMetadata } = studies;
|
||||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
const { studyMetadataManager, updateMetaDataManager } = utils;
|
||||||
|
|
||||||
@@ -13,65 +13,97 @@ class ViewerRetrieveStudyData extends Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
studyInstanceUids: PropTypes.array.isRequired,
|
studyInstanceUids: PropTypes.array.isRequired,
|
||||||
seriesInstanceUids: PropTypes.array,
|
seriesInstanceUids: PropTypes.array,
|
||||||
server: PropTypes.object
|
server: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
studies: null,
|
super(props);
|
||||||
error: null
|
this.state = {
|
||||||
};
|
studies: null,
|
||||||
|
error: null,
|
||||||
async componentDidMount() {
|
};
|
||||||
// TODO: Avoid using timepoints here
|
}
|
||||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
|
||||||
const { studyInstanceUids, seriesInstanceUids, server } = this.props;
|
|
||||||
|
|
||||||
|
async loadStudies() {
|
||||||
try {
|
try {
|
||||||
|
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
|
||||||
const studies = await retrieveStudiesMetadata(
|
const studies = await retrieveStudiesMetadata(
|
||||||
server,
|
server,
|
||||||
studyInstanceUids,
|
studyInstanceUids,
|
||||||
seriesInstanceUids
|
seriesInstanceUids
|
||||||
);
|
);
|
||||||
|
this.setStudies(studies);
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({ error: true });
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render the viewer when the data is ready
|
setStudies(givenStudies) {
|
||||||
// TODO: CLEAR THIS SOMEWHERE ELSE
|
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
|
||||||
studyMetadataManager.purge();
|
const sopClassHandlerModules =
|
||||||
|
extensionManager.modules['sopClassHandlerModule'];
|
||||||
// Map studies to new format, update metadata manager?
|
// Map studies to new format, update metadata manager?
|
||||||
const updatedStudies = studies.map(study => {
|
const studies = givenStudies.map(study => {
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
const studyMetadata = new OHIFStudyMetadata(
|
||||||
study,
|
study,
|
||||||
study.studyInstanceUid
|
study.studyInstanceUid
|
||||||
);
|
);
|
||||||
const sopClassHandlerModules =
|
if (!study.displaySets) {
|
||||||
extensionManager.modules["sopClassHandlerModule"];
|
study.displaySets = studyMetadata.createDisplaySets(
|
||||||
|
sopClassHandlerModules
|
||||||
study.displaySets =
|
);
|
||||||
study.displaySets ||
|
}
|
||||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
studyMetadata.setDisplaySets(study.displaySets);
|
||||||
|
|
||||||
// Updates WADO-RS metaDataManager
|
// Updates WADO-RS metaDataManager
|
||||||
updateMetaDataManager(study);
|
updateMetaDataManager(study);
|
||||||
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
studyMetadataManager.add(studyMetadata);
|
||||||
|
// Attempt to load remaning series if any
|
||||||
|
this._attemptToLoadRemainingSeries(studyMetadata);
|
||||||
return study;
|
return study;
|
||||||
});
|
});
|
||||||
|
this.setState({ studies });
|
||||||
this.setState({
|
|
||||||
studies: updatedStudies
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({
|
|
||||||
error: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Handle gracefully instead of throwing?
|
|
||||||
throw new Error(err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addSeriesToStudy(studyMetadata, series) {
|
||||||
|
const sopClassHandlerModules =
|
||||||
|
extensionManager.modules['sopClassHandlerModule'];
|
||||||
|
const study = studyMetadata.getData();
|
||||||
|
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||||
|
studyMetadata.addSeries(seriesMetadata);
|
||||||
|
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||||
|
sopClassHandlerModules,
|
||||||
|
seriesMetadata
|
||||||
|
);
|
||||||
|
study.displaySets = studyMetadata.getDisplaySets();
|
||||||
|
updateMetaDataManager(study, series.seriesInstanceUid);
|
||||||
|
this.setState(function(state) {
|
||||||
|
return { studies: state.studies.slice() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_attemptToLoadRemainingSeries(studyMetadata) {
|
||||||
|
const { seriesLoader } = studyMetadata.getData();
|
||||||
|
if (!seriesLoader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (seriesLoader.hasNext()) {
|
||||||
|
seriesLoader
|
||||||
|
.next()
|
||||||
|
.then(
|
||||||
|
series => void this._addSeriesToStudy(studyMetadata, series),
|
||||||
|
error => void log.error(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// TODO: CLEAR THIS SOMEWHERE ELSE
|
||||||
|
studyMetadataManager.purge();
|
||||||
|
this.loadStudies();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.error) {
|
if (this.state.error) {
|
||||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||||
|
|||||||
@@ -2272,6 +2272,16 @@
|
|||||||
once "^1.4.0"
|
once "^1.4.0"
|
||||||
universal-user-agent "^4.0.0"
|
universal-user-agent "^4.0.0"
|
||||||
|
|
||||||
|
"@ohif/extension-cornerstone@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.0.0.tgz#b4ee3b594212502192cdd311a100354857280548"
|
||||||
|
integrity sha512-hvJ3t2GRcu906NiYpUWaJIB9ja/M/MKEYLy19t4XBFpqf4VqoezR2KJpZelmZjQscXGaDQeZgvr3DJO5Nva8Uw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
classnames "^2.2.6"
|
||||||
|
lodash.throttle "^4.1.1"
|
||||||
|
react-cornerstone-viewport "0.1.30"
|
||||||
|
|
||||||
"@ohif/i18n@^0.2.3":
|
"@ohif/i18n@^0.2.3":
|
||||||
version "0.2.5"
|
version "0.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@ohif/i18n/-/i18n-0.2.5.tgz#2b92a78109823ce0f50dd590201d980a5fd64974"
|
resolved "https://registry.yarnpkg.com/@ohif/i18n/-/i18n-0.2.5.tgz#2b92a78109823ce0f50dd590201d980a5fd64974"
|
||||||
@@ -8273,6 +8283,11 @@ gl-matrix@3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.0.0.tgz#888301ac7650e148c3865370e13ec66d08a8381f"
|
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.0.0.tgz#888301ac7650e148c3865370e13ec66d08a8381f"
|
||||||
integrity sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA==
|
integrity sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA==
|
||||||
|
|
||||||
|
gl-matrix@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.1.0.tgz#f5b2de17d8fed95a79e5025b10cded0ab9ccbed0"
|
||||||
|
integrity sha512-526NA+3EA+ztAQi0IZpSWiM0fyQXIp7IbRvfJ4wS/TjjQD0uv0fVybXwwqqSOlq33UckivI0yMDlVtboWm3k7A==
|
||||||
|
|
||||||
gl-preserve-state@^1.0.0:
|
gl-preserve-state@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198"
|
resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198"
|
||||||
@@ -14954,12 +14969,13 @@ react-transition-group@^4.1.1:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
react-vtkjs-viewport@0.1.0:
|
react-vtkjs-viewport@0.1.4:
|
||||||
version "0.1.0"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.1.0.tgz#a00b114b635ff95a82ae1227d70c248e4cafddf4"
|
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.1.4.tgz#f5ecc7dd52ab065d54656f36851f49b058ba683f"
|
||||||
integrity sha512-0gnRfntpV6ZBNvZOHzjHapFeJajO8nfkSvO5SNUu41bkyXxqYcJwZvb3EWbd04V1RnA0AnKRoR/KL+I09Os7gg==
|
integrity sha512-07hVFJJ9LTy+Z9dgdPiPkaohVb013bgjNgH29MVafnjwH92YOXJzfFbf8/yzzVIZsYCETFf7rVw+oMGQJs5TAA==
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns "^2.2.1"
|
date-fns "^2.2.1"
|
||||||
|
gl-matrix "^3.1.0"
|
||||||
|
|
||||||
react-with-direction@1.3.0:
|
react-with-direction@1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
|
|||||||
Reference in new issue
Block a user