Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb2088daf7 | ||
|
|
2a743554b6 | ||
|
|
a28984ef1f | ||
|
|
95e5e11d09 | ||
|
|
ff9f199850 | ||
|
|
1cc94f36a7 | ||
|
|
5c5223fcf1 | ||
|
|
8513ad9ef3 | ||
|
|
66d8bc6d6f | ||
|
|
0064a4b1cd | ||
|
|
74ce89b22b | ||
|
|
c92975f555 | ||
|
|
ce6c0533a0 | ||
|
|
4df624b8c4 | ||
|
|
e064fd353e | ||
|
|
458d310c78 | ||
|
|
73bc4d9094 | ||
|
|
7709ebb107 | ||
|
|
559b637290 | ||
|
|
36910f2b47 | ||
|
|
9e10c2b2de |
No files matched your search
@@ -142,8 +142,8 @@ jobs:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
# - setup_remote_docker:
|
||||
# docker_layer_caching: true
|
||||
- run:
|
||||
name: Build and push Docker image
|
||||
command: |
|
||||
@@ -198,8 +198,8 @@ jobs:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
# - setup_remote_docker:
|
||||
# docker_layer_caching: true
|
||||
- run:
|
||||
name: Deploy latest version to viewer.ohif.org
|
||||
command: |
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.0...@ohif/extension-cornerstone@1.3.1) (2019-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 set tools bidirectional, eraser and annotate command ([#1020](https://github.com/OHIF/Viewers/issues/1020)) ([a28984e](https://github.com/OHIF/Viewers/commit/a28984e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.5...@ohif/extension-cornerstone@1.3.0) (2019-10-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.2.4...@ohif/extension-cornerstone@1.2.5) (2019-09-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.2.5",
|
||||
"version": "1.3.1",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -48,6 +48,6 @@
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-cornerstone-viewport": "0.1.30"
|
||||
"react-cornerstone-viewport": "2.x.x"
|
||||
}
|
||||
}
|
||||
@@ -23,36 +23,40 @@ const MEASUREMENT_ACTION_MAP = {
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
let dataFromStore;
|
||||
|
||||
// TODO: This may not be updated anymore :thinking:
|
||||
if (state.extensions && state.extensions.cornerstone) {
|
||||
dataFromStore = state.extensions.cornerstone;
|
||||
}
|
||||
|
||||
// TODO: This should be extension configuration
|
||||
// ...dataFromStore -->
|
||||
// availableTools,
|
||||
// onNewImage,
|
||||
// onRightClick,
|
||||
// onTouchPress,
|
||||
// onTouchStart,
|
||||
// onMouseClick,
|
||||
|
||||
// If this is the active viewport, enable prefetching.
|
||||
const { viewportIndex } = ownProps; //.viewportData;
|
||||
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
||||
const viewportSpecificData =
|
||||
state.viewports.viewportSpecificData[viewportIndex] || {};
|
||||
|
||||
// CINE
|
||||
let isPlaying = false;
|
||||
let frameRate = 24;
|
||||
|
||||
if (viewportSpecificData && viewportSpecificData.cine) {
|
||||
const cine = viewportSpecificData.cine;
|
||||
|
||||
isPlaying = cine.isPlaying === true;
|
||||
frameRate = cine.cineFrameRate || frameRate;
|
||||
}
|
||||
|
||||
return {
|
||||
layout: state.viewports.layout,
|
||||
// layout: state.viewports.layout,
|
||||
isActive,
|
||||
// TODO: Need a cleaner and more versatile way.
|
||||
// Currently justing using escape hatch + commands
|
||||
// activeTool: activeButton && activeButton.command,
|
||||
...dataFromStore,
|
||||
enableStackPrefetch: isActive,
|
||||
isStackPrefetchEnabled: isActive,
|
||||
isPlaying,
|
||||
frameRate,
|
||||
//stack: viewportSpecificData.stack,
|
||||
cineToolData: viewportSpecificData.cine,
|
||||
viewport: viewportSpecificData.viewport,
|
||||
// viewport: viewportSpecificData.viewport,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { Component } from "react";
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import ConnectedCornerstoneViewport from "./ConnectedCornerstoneViewport";
|
||||
import OHIF from "@ohif/core";
|
||||
import PropTypes from "prop-types";
|
||||
import cornerstone from "cornerstone-core";
|
||||
import handleSegmentationStorage from "./handleSegmentationStorage.js";
|
||||
import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
@@ -18,7 +18,7 @@ cornerstone.metaData.addProvider(
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
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 = {};
|
||||
@@ -28,11 +28,11 @@ specialCaseHandlers[
|
||||
|
||||
class OHIFCornerstoneViewport extends Component {
|
||||
state = {
|
||||
viewportData: null
|
||||
viewportData: null,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
customProps: {}
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -40,17 +40,17 @@ class OHIFCornerstoneViewport extends Component {
|
||||
displaySet: PropTypes.object,
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object
|
||||
customProps: PropTypes.object,
|
||||
};
|
||||
|
||||
static id = "OHIFCornerstoneViewport";
|
||||
static id = 'OHIFCornerstoneViewport';
|
||||
|
||||
static init() {
|
||||
console.log("OHIFCornerstoneViewport init()");
|
||||
console.log('OHIFCornerstoneViewport init()');
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
console.log("OHIFCornerstoneViewport destroy()");
|
||||
console.log('OHIFCornerstoneViewport destroy()');
|
||||
StackManager.clearStacks();
|
||||
}
|
||||
|
||||
@@ -72,15 +72,15 @@ class OHIFCornerstoneViewport extends Component {
|
||||
frameIndex = 0
|
||||
) {
|
||||
if (!studies || !studies.length) {
|
||||
throw new Error("Studies not provided.");
|
||||
throw new Error('Studies not provided.');
|
||||
}
|
||||
|
||||
if (!studyInstanceUid) {
|
||||
throw new Error("StudyInstanceUID not provided.");
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
if (!displaySetInstanceUid) {
|
||||
throw new Error("StudyInstanceUID not provided.");
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
// Create shortcut to displaySet
|
||||
@@ -89,7 +89,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
throw new Error("Study not found.");
|
||||
throw new Error('Study not found.');
|
||||
}
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
@@ -97,7 +97,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
throw new Error("Display Set not found.");
|
||||
throw new Error('Display Set not found.');
|
||||
}
|
||||
|
||||
// Get stack from Stack Manager
|
||||
@@ -110,7 +110,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
if (sopInstanceUid) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
"sopCommonModule",
|
||||
'sopCommonModule',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
@@ -124,7 +124,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
stack.currentImageIdIndex = index;
|
||||
} else {
|
||||
console.warn(
|
||||
"SOPInstanceUID provided was not found in specified DisplaySet"
|
||||
'SOPInstanceUID provided was not found in specified DisplaySet'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -132,22 +132,6 @@ class OHIFCornerstoneViewport extends Component {
|
||||
return stack;
|
||||
}
|
||||
|
||||
static getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
) => {
|
||||
return OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
};
|
||||
|
||||
getViewportData = async (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
@@ -172,7 +156,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
);
|
||||
break;
|
||||
default:
|
||||
const stack = OHIFCornerstoneViewport.getViewportData(
|
||||
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
@@ -183,7 +167,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
viewportData = {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack
|
||||
stack,
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -199,7 +183,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||
@@ -208,7 +192,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
console.warn(
|
||||
"More than one SOPClassUid in the same series is not yet supported."
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -223,7 +207,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
frameIndex
|
||||
).then(viewportData => {
|
||||
this.setState({
|
||||
viewportData
|
||||
viewportData,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -249,25 +233,42 @@ class OHIFCornerstoneViewport extends Component {
|
||||
render() {
|
||||
let childrenWithProps = null;
|
||||
|
||||
if (!this.state.viewportData) {
|
||||
return null;
|
||||
}
|
||||
const { viewportIndex } = this.props;
|
||||
const {
|
||||
imageIds,
|
||||
currentImageIdIndex,
|
||||
// If this comes from the instance, would be a better default
|
||||
// `FrameTime` in the instance
|
||||
// frameRate = 0,
|
||||
} = this.state.viewportData.stack;
|
||||
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index
|
||||
key: index,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.viewportData && (
|
||||
<ConnectedCornerstoneViewport
|
||||
viewportData={this.state.viewportData}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
{...this.props.customProps}
|
||||
/>
|
||||
)}
|
||||
<ConnectedCornerstoneViewport
|
||||
viewportIndex={viewportIndex}
|
||||
imageIds={imageIds}
|
||||
imageIdIndex={currentImageIdIndex}
|
||||
// ~~ Connected (From REDUX)
|
||||
// frameRate={frameRate}
|
||||
// isPlaying={false}
|
||||
// isStackPrefetchEnabled={true}
|
||||
// onElementEnabled={() => {}}
|
||||
// setViewportActive{() => {}}
|
||||
{...this.props.customProps}
|
||||
/>
|
||||
{childrenWithProps}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import init from './init.js';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
|
||||
const OHIFCornerstoneViewport = asyncComponent(() => import( /* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'));
|
||||
const OHIFCornerstoneViewport = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "OHIFCornerstoneViewport" */ './OHIFCornerstoneViewport.js'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -13,6 +18,15 @@ export default {
|
||||
*/
|
||||
id: 'cornerstone',
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration(configuration = {}) {
|
||||
init(configuration);
|
||||
},
|
||||
getViewportModule() {
|
||||
return OHIFCornerstoneViewport;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init(configuration = {}) {
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
// ~~ Set our MetadataProvider
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
// ~~
|
||||
const defaultCsToolsConfig = csToolsConfig || {
|
||||
globalToolSyncEnabled: true,
|
||||
showSVGCursors: true,
|
||||
autoResizeViewports: false,
|
||||
};
|
||||
|
||||
initCornerstoneTools(defaultCsToolsConfig);
|
||||
|
||||
// ~~ Toooools 🙌
|
||||
const {
|
||||
PanTool,
|
||||
ZoomTool,
|
||||
WwwcTool,
|
||||
MagnifyTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
PanMultiTouchTool,
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
ArrowAnnotateTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
FreehandRoiTool,
|
||||
EllipticalRoiTool,
|
||||
DragProbeTool,
|
||||
RectangleRoiTool,
|
||||
// Segmentation
|
||||
BrushTool,
|
||||
} = csTools;
|
||||
const tools = [
|
||||
PanTool,
|
||||
ZoomTool,
|
||||
WwwcTool,
|
||||
MagnifyTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
PanMultiTouchTool,
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
ArrowAnnotateTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
FreehandRoiTool,
|
||||
EllipticalRoiTool,
|
||||
DragProbeTool,
|
||||
RectangleRoiTool,
|
||||
// Segmentation
|
||||
BrushTool,
|
||||
];
|
||||
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
|
||||
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
||||
csTools.setToolActive('ZoomTouchPinch', {});
|
||||
}
|
||||
File renamed without changes.
@@ -74,7 +74,7 @@ const definitions = [
|
||||
label: 'Annotate',
|
||||
icon: 'measure-non-target',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'ArrowAnnotate' },
|
||||
},
|
||||
@@ -200,7 +200,7 @@ const definitions = [
|
||||
label: 'Bidirectional',
|
||||
icon: 'measure-target',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
},
|
||||
@@ -209,7 +209,7 @@ const definitions = [
|
||||
label: 'Eraser',
|
||||
icon: 'eraser',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Eraser' },
|
||||
},
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.2...@ohif/extension-vtk@0.52.3) (2019-10-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.1...@ohif/extension-vtk@0.52.2) (2019-10-09)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* 🎸 Update vtk viewport for faster image reconstruction ([#1016](https://github.com/OHIF/Viewers/issues/1016)) ([8513ad9](https://github.com/OHIF/Viewers/commit/8513ad9))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.51.2",
|
||||
"version": "0.52.3",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -48,11 +48,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"react-vtkjs-viewport": "0.1.2"
|
||||
"react-vtkjs-viewport": "0.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.0.2",
|
||||
"@ohif/ui": "^0.52.0",
|
||||
"@ohif/core": "^1.2.0",
|
||||
"@ohif/ui": "^0.53.2",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { Component } from "react";
|
||||
import { getImageData, loadImageData } from "react-vtkjs-viewport";
|
||||
import React, { Component } from 'react';
|
||||
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||
|
||||
import ConnectedVTKViewport from "./ConnectedVTKViewport";
|
||||
import LoadingIndicator from "./LoadingIndicator.js";
|
||||
import OHIF from "@ohif/core";
|
||||
import PropTypes from "prop-types";
|
||||
import cornerstone from "cornerstone-core";
|
||||
import handleSegmentationStorage from "./handleSegmentationStorage.js";
|
||||
import vtkDataArray from "vtk.js/Sources/Common/Core/DataArray";
|
||||
import vtkImageData from "vtk.js/Sources/Common/DataModel/ImageData";
|
||||
import vtkVolume from "vtk.js/Sources/Rendering/Core/Volume";
|
||||
import vtkVolumeMapper from "vtk.js/Sources/Rendering/Core/VolumeMapper";
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import LoadingIndicator from './LoadingIndicator.js';
|
||||
import OHIF from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
|
||||
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
|
||||
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
@@ -24,7 +25,7 @@ cornerstone.metaData.addProvider(
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
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 = {};
|
||||
@@ -42,7 +43,7 @@ const volumeCache = {};
|
||||
*/
|
||||
function createLabelMapImageData(backgroundImageData) {
|
||||
const labelMapData = vtkImageData.newInstance(
|
||||
backgroundImageData.get("spacing", "origin", "direction")
|
||||
backgroundImageData.get('spacing', 'origin', 'direction')
|
||||
);
|
||||
labelMapData.setDimensions(backgroundImageData.getDimensions());
|
||||
labelMapData.computeTransforms();
|
||||
@@ -50,7 +51,7 @@ function createLabelMapImageData(backgroundImageData) {
|
||||
const values = new Uint8Array(backgroundImageData.getNumberOfPoints());
|
||||
const dataArray = vtkDataArray.newInstance({
|
||||
numberOfComponents: 1, // labelmap with single component
|
||||
values
|
||||
values,
|
||||
});
|
||||
labelMapData.getPointData().setScalars(dataArray);
|
||||
|
||||
@@ -61,24 +62,24 @@ class OHIFVTKViewport extends Component {
|
||||
state = {
|
||||
volumes: null,
|
||||
paintFilterLabelMapImageData: null,
|
||||
paintFilterBackgroundImageData: null
|
||||
paintFilterBackgroundImageData: null,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static id = "OHIFVTKViewport";
|
||||
static id = 'OHIFVTKViewport';
|
||||
|
||||
static init() {
|
||||
console.log("OHIFVTKViewport init()");
|
||||
console.log('OHIFVTKViewport init()');
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
console.log("OHIFVTKViewport destroy()");
|
||||
console.log('OHIFVTKViewport destroy()');
|
||||
StackManager.clearStacks();
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ class OHIFVTKViewport extends Component {
|
||||
} else if (sopInstanceUid) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
"sopCommonModule",
|
||||
'sopCommonModule',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
@@ -151,7 +152,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
throw new Error("Not yet implemented");
|
||||
throw new Error('Not yet implemented');
|
||||
|
||||
const data = handleSegmentationStorage(
|
||||
stack.imageIds,
|
||||
@@ -164,7 +165,7 @@ class OHIFVTKViewport extends Component {
|
||||
return loadImageData(imageDataObject).then(() => {
|
||||
return {
|
||||
data: imageDataObject.vtkImageData,
|
||||
labelmap: labelmapDataObject
|
||||
labelmap: labelmapDataObject,
|
||||
};
|
||||
});
|
||||
default:
|
||||
@@ -172,7 +173,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
return loadImageData(imageDataObject).then(() => {
|
||||
return {
|
||||
data: imageDataObject.vtkImageData
|
||||
data: imageDataObject.vtkImageData,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -189,10 +190,16 @@ class OHIFVTKViewport extends Component {
|
||||
volumeActor.setMapper(volumeMapper);
|
||||
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
|
||||
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
|
||||
const sampleDistance =
|
||||
@@ -200,7 +207,7 @@ class OHIFVTKViewport extends Component {
|
||||
Math.sqrt(
|
||||
data
|
||||
.getSpacing()
|
||||
.map((v) => v * v)
|
||||
.map(v => v * v)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
);
|
||||
|
||||
@@ -218,12 +225,12 @@ class OHIFVTKViewport extends Component {
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (sopClassUids.length > 1) {
|
||||
console.warn(
|
||||
"More than one SOPClassUid in the same series is not yet supported."
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,7 +256,7 @@ class OHIFVTKViewport extends Component {
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
paintFilterBackgroundImageData: data,
|
||||
paintFilterLabelMapImageData: labelmap
|
||||
paintFilterLabelMapImageData: labelmap,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,6 +264,11 @@ class OHIFVTKViewport extends Component {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
console.log(this.props.viewportIndex);
|
||||
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
@@ -279,12 +291,12 @@ class OHIFVTKViewport extends Component {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
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 (
|
||||
<>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
|
||||
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
|
||||
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
|
||||
@@ -263,16 +264,19 @@ const actions = {
|
||||
|
||||
apis = apiByViewport;
|
||||
|
||||
/*const rgbTransferFunction = apiByViewport[0].volumes[0]
|
||||
const rgbTransferFunction = apiByViewport[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
rgbTransferFunction.onModified(() => {
|
||||
|
||||
const onModifiedSubscription = rgbTransferFunction.onModified(() => {
|
||||
apiByViewport.forEach(a => {
|
||||
const renderWindow = a.genericRenderWindow.getRenderWindow();
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
});*/
|
||||
});
|
||||
|
||||
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
|
||||
|
||||
apiByViewport.forEach((api, index) => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
@@ -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;
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.0",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -9,7 +9,7 @@ import user from '../user';
|
||||
* @export
|
||||
* @param {Object} [server={}]
|
||||
* @param {Object} [server.requestOptions]
|
||||
* @param {string} [server.requestOptions.auth]
|
||||
* @param {string|function} [server.requestOptions.auth]
|
||||
* @returns {Object} { Authorization }
|
||||
*/
|
||||
export default function getAuthorizationHeader({ requestOptions } = {}) {
|
||||
@@ -19,8 +19,13 @@ export default function getAuthorizationHeader({ requestOptions } = {}) {
|
||||
const accessToken = user && user.getAccessToken && user.getAccessToken();
|
||||
|
||||
if (requestOptions && requestOptions.auth) {
|
||||
// HTTP Basic Auth (user:password)
|
||||
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
||||
if (typeof requestOptions.auth === 'function') {
|
||||
// Custom Auth Header
|
||||
headers.Authorization = requestOptions.auth(requestOptions);
|
||||
} else {
|
||||
// HTTP Basic Auth (user:password)
|
||||
headers.Authorization = `Basic ${btoa(requestOptions.auth)}`;
|
||||
}
|
||||
} else if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ describe('getAuthorizationHeader', () => {
|
||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth', () => {
|
||||
const validServer = {
|
||||
requestOptions: {
|
||||
auth: {
|
||||
user: 'dummy_user',
|
||||
password: 'dummy_password',
|
||||
},
|
||||
auth: 'dummy_user: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', () => {
|
||||
const validServerWithoutPassword = {
|
||||
requestOptions: {
|
||||
auth: {
|
||||
user: 'dummy_user',
|
||||
},
|
||||
auth: 'dummy_user',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,22 +38,19 @@ describe('getAuthorizationHeader', () => {
|
||||
expect(authentication).toEqual(expectedAuthorizationHeader);
|
||||
});
|
||||
|
||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth even though there is no username', () => {
|
||||
const validServerWithoutPassword = {
|
||||
it('should return a HTTP Basic Auth when server contains requestOptions.auth custom function', () => {
|
||||
const validServerCustomAuth = {
|
||||
requestOptions: {
|
||||
auth: {
|
||||
user: 'dummy_user',
|
||||
},
|
||||
auth: options => `Basic ${options.token}`,
|
||||
token: 'ZHVtbXlfdXNlcjpkdW1teV9wYXNzd29yZA==',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedAuthorizationHeader = {
|
||||
Authorization: `Basic ${btoa(
|
||||
validServerWithoutPassword.requestOptions.auth
|
||||
)}`,
|
||||
Authorization: `Basic ${validServerCustomAuth.requestOptions.token}`,
|
||||
};
|
||||
|
||||
const authentication = getAuthorizationHeader(validServerWithoutPassword);
|
||||
const authentication = getAuthorizationHeader(validServerCustomAuth);
|
||||
|
||||
expect(authentication).toEqual(expectedAuthorizationHeader);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// import { api } from 'dicomweb-client'
|
||||
|
||||
const api = {
|
||||
DICOMwebClient: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
DICOMwebClient: jest.fn().mockImplementation(function() {
|
||||
this.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
|
||||
import { api } from 'dicomweb-client';
|
||||
// - createStacks
|
||||
import { isImage } from './../../utils/isImage';
|
||||
import { isImage } from '../../utils/isImage';
|
||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||
|
||||
export class StudyMetadata extends Metadata {
|
||||
constructor(data, uid) {
|
||||
@@ -88,6 +89,100 @@ export class StudyMetadata extends Metadata {
|
||||
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
|
||||
* The series that appear in the Study Metadata must represent
|
||||
@@ -101,105 +196,44 @@ export class StudyMetadata extends Metadata {
|
||||
* @returns {Array} An array of series to be placed in the Study Metadata
|
||||
*/
|
||||
createDisplaySets(sopClassHandlerModules) {
|
||||
const study = this;
|
||||
const displaySets = [];
|
||||
const anyDisplaySets = study.getSeriesCount();
|
||||
const anySopClassHandlerModules =
|
||||
sopClassHandlerModules && sopClassHandlerModules.length > 0;
|
||||
const anyDisplaySets = this.getSeriesCount();
|
||||
|
||||
if (!anyDisplaySets) {
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
// Loop through the series (SeriesMetadata)
|
||||
this.forEachSeries(series => {
|
||||
const anyInstances = series.getInstanceCount() > 0;
|
||||
if (!anyInstances) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sopClassUids = getSopClassUids(series);
|
||||
|
||||
if (anySopClassHandlerModules) {
|
||||
const displaySet = _getDisplaySetFromSopClassModule(
|
||||
this.forEachSeries(
|
||||
series =>
|
||||
void this._createDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
series,
|
||||
study,
|
||||
sopClassUids
|
||||
);
|
||||
displaySets
|
||||
)
|
||||
);
|
||||
|
||||
if (displaySet) {
|
||||
displaySet.sopClassModule = true;
|
||||
displaySets.push(displaySet);
|
||||
return sortDisplaySetList(displaySets);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
displaySets.sort(_sortBySeriesNumber);
|
||||
displaySets.sort(_sortSegToEndOfList);
|
||||
|
||||
return displaySets;
|
||||
/**
|
||||
* 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
|
||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
||||
*/
|
||||
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||
if (this.containsSeries(series)) {
|
||||
this.setDisplaySets(
|
||||
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +241,10 @@ export class StudyMetadata extends Metadata {
|
||||
* @param {Array} displaySets Array of display sets (ImageSet[])
|
||||
*/
|
||||
setDisplaySets(displaySets) {
|
||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||
this.sortDisplaySets();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,6 +358,12 @@ export class StudyMetadata extends Metadata {
|
||||
return found;
|
||||
}
|
||||
|
||||
containsSeries(series) {
|
||||
return (
|
||||
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the number of series within the current study.
|
||||
* @returns {number} The number of series in the current study.
|
||||
@@ -651,11 +694,41 @@ 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 {*} 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 =
|
||||
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
|
||||
|
||||
@@ -663,22 +736,9 @@ function _sortBySeriesNumber(a, b) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Move Segmentation modality files to the end of the list of
|
||||
* display sets. This is a workaround to prevent issues when
|
||||
* the referenced dataset's metadata is not yet available.
|
||||
*
|
||||
* It will be removed once proper SEG ingestion is added.
|
||||
*
|
||||
* @param {*} a - DisplaySet
|
||||
* @param {*} b - DisplaySet
|
||||
* Sorts a list of display set objects
|
||||
* @param {Array} list A list of display sets to be sorted
|
||||
*/
|
||||
function _sortSegToEndOfList(a, b) {
|
||||
const displaySetAIsSeg = a.modality === 'SEG';
|
||||
const displaySetBIsSeg = b.modality === 'SEG';
|
||||
|
||||
if (displaySetAIsSeg && displaySetBIsSeg) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return displaySetAIsSeg ? 1 : -1;
|
||||
}
|
||||
function sortDisplaySetList(list) {
|
||||
return list.sort(seriesSortingCriteria);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
|
||||
|
||||
const INFO = Symbol('INFO');
|
||||
|
||||
const WADOProxy = {
|
||||
convertURL: (url, server) => {
|
||||
@@ -94,9 +97,7 @@ function buildInstanceWadoRsUri(
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
) {
|
||||
return `${
|
||||
server.wadoRoot
|
||||
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||
}
|
||||
|
||||
function buildInstanceFrameWadoRsUri(
|
||||
@@ -166,9 +167,9 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
|
||||
};
|
||||
|
||||
const arrayBufferToPaletteColorLUT = result => {
|
||||
const arraybuffer = result[0]
|
||||
const byteArray = new Uint8Array(arraybuffer)
|
||||
const lut = []
|
||||
const arraybuffer = result[0];
|
||||
const byteArray = new Uint8Array(arraybuffer);
|
||||
const lut = [];
|
||||
|
||||
for (let i = 0; i < numLutEntries; i++) {
|
||||
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
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
* @param {Object} server Object with server configuration paramenters
|
||||
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
|
||||
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
|
||||
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
|
||||
const firstSopInstance = sopInstanceList[0];
|
||||
const study = createStudy(server, firstSopInstance);
|
||||
await addInstancesToStudy(server, study, sopInstanceList);
|
||||
return study;
|
||||
}
|
||||
|
||||
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;
|
||||
throw new Error('Failed to create study out of provided SOP instance list');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Study MetaData from a DICOM server using a WADO call
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {Promise}
|
||||
* 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 {Object} server Object with server configuration paramenters
|
||||
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
|
||||
* @returns {Object} A study descriptor object
|
||||
*/
|
||||
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,
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
studyInstanceUID: studyInstanceUid,
|
||||
};
|
||||
});
|
||||
return dicomWeb
|
||||
.retrieveStudyMetadata({ studyInstanceUID })
|
||||
.then(result => createStudyFromSOPInstanceList(server, result));
|
||||
}
|
||||
|
||||
return dicomWeb.retrieveStudyMetadata(options).then(result => {
|
||||
return resultDataToStudyMetadata(server, studyInstanceUid, result);
|
||||
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
|
||||
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;
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
export default function updateMetaDataManager(study) {
|
||||
study.seriesList.forEach(series => {
|
||||
series.instances.forEach(instance => {
|
||||
// Cache just images that are going to be loaded via WADO-RS
|
||||
if (
|
||||
instance.imageRendering !== 'wadors' &&
|
||||
instance.thumbnailRendering !== 'wadors'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Update metadata manager with instances of a specifc series
|
||||
* @param {Object} study A plain study descriptor object
|
||||
* @param {Object} series A Series descriptor object contaning the instances to be added to the manager
|
||||
*/
|
||||
function updateMetaDataManagerForSeries(study, series) {
|
||||
series.instances.forEach(instance => {
|
||||
// Cache just images that are going to be loaded via WADO-RS
|
||||
if (
|
||||
instance.imageRendering !== 'wadors' &&
|
||||
instance.thumbnailRendering !== 'wadors'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
||||
const numberOfFrames = instance.numberOfFrames || 1;
|
||||
const metaData = getWadoRsInstanceMetaData(study, series, instance);
|
||||
const numberOfFrames = instance.numberOfFrames || 1;
|
||||
|
||||
// We can share the same metaData with all frames because it doesn't have
|
||||
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
||||
// WADO-RS frame number is 1-based
|
||||
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
||||
const imageId = getWADORSImageId(instance, frameNumber);
|
||||
// We can share the same metaData with all frames because it doesn't have
|
||||
// any frame specific data, such as frameNumber, pixelData, offset, etc.
|
||||
// WADO-RS frame number is 1-based
|
||||
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
|
||||
const imageId = getWADORSImageId(instance, frameNumber);
|
||||
|
||||
// TODO Drop dependency on this
|
||||
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
|
||||
imageId,
|
||||
metaData
|
||||
);
|
||||
}
|
||||
});
|
||||
// TODO Drop dependency on this
|
||||
cornerstoneWADOImageLoader.wadors.metaDataManager.add(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.
|
||||
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)
|
||||
|
||||
**Note:** Version bump only for package @ohif/i18n
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.50.4",
|
||||
"version": "0.50.5",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"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)",
|
||||
"Previous image": "$t(Common:Previous) $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"
|
||||
}
|
||||
@@ -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",
|
||||
"Previous image": "$t(Common:Image) $t(Common:Previous)",
|
||||
"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)",
|
||||
"Previous image": "$t(Common:Previous) $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)",
|
||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||
"Skip to first image": "Bỏ qua đến đầu $t(Common:Image)",
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.1...@ohif/ui@0.53.2) (2019-10-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.52.0",
|
||||
"version": "0.53.2",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global cornerstone */
|
||||
import './ImageThumbnail.styl';
|
||||
|
||||
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?
|
||||
//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 {
|
||||
static propTypes = {
|
||||
imageSrc: PropTypes.string,
|
||||
@@ -52,40 +34,32 @@ export default class ImageThumbnail extends PureComponent {
|
||||
super(props);
|
||||
|
||||
this.canvas = React.createRef();
|
||||
|
||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
||||
|
||||
this.state = {
|
||||
loading: renderIntoCanvas,
|
||||
loading: this.shouldRenderToCanvas(),
|
||||
};
|
||||
}
|
||||
|
||||
shouldRenderToCanvas() {
|
||||
return this.props.imageId && !this.props.imageSrc;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
||||
|
||||
if (renderIntoCanvas) {
|
||||
const { imageId } = this.props;
|
||||
const canvas = this.canvas.current;
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(
|
||||
image => {
|
||||
renderAsync(canvas, image).then(
|
||||
() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
},
|
||||
error => {
|
||||
// TODO: Set state?
|
||||
throw new Error(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
error => {
|
||||
// TODO: Set state?
|
||||
if (this.shouldRenderToCanvas()) {
|
||||
cornerstone
|
||||
.loadAndCacheImage(this.props.imageId)
|
||||
.then(image => {
|
||||
cornerstone.renderToCanvas(this.canvas.current, image);
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
});
|
||||
throw new Error(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +74,10 @@ export default class ImageThumbnail extends PureComponent {
|
||||
const showStackLoadingProgressBar =
|
||||
this.props.stackPercentComplete !== undefined;
|
||||
|
||||
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
|
||||
|
||||
return (
|
||||
<div className="ImageThumbnail">
|
||||
<div className="image-thumbnail-canvas">
|
||||
{renderIntoCanvas ? (
|
||||
{this.shouldRenderToCanvas() ? (
|
||||
<canvas
|
||||
ref={this.canvas}
|
||||
width={this.props.width}
|
||||
@@ -130,6 +102,9 @@ export default class ImageThumbnail extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{this.state.loading && (
|
||||
<div className="image-thumbnail-loading-indicator"></div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,36 @@
|
||||
-webkit-background-clip: padding
|
||||
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
|
||||
height: 100%
|
||||
overflow: hidden
|
||||
@@ -36,7 +66,6 @@
|
||||
-webkit-user-drag: none
|
||||
pointer-events: none
|
||||
|
||||
|
||||
.thumbnailLoadingIndicator
|
||||
display: none
|
||||
pointer-events: none
|
||||
|
||||
@@ -25,7 +25,7 @@ class StudyBrowser extends Component {
|
||||
if (this.props.supportsDragAndDrop) {
|
||||
return (
|
||||
<ThumbnailEntryDragSource
|
||||
key={`${studyIndex}_${thumbIndex}`}
|
||||
key={thumb.displaySetInstanceUid}
|
||||
{...study}
|
||||
{...thumb}
|
||||
id={`${studyIndex}_${thumbIndex}`}
|
||||
@@ -35,9 +35,9 @@ class StudyBrowser extends Component {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="ThumbnailEntryContainer">
|
||||
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
|
||||
<ThumbnailEntry
|
||||
key={`${studyIndex}_${thumbIndex}`}
|
||||
key={thumb.displaySetInstanceUid}
|
||||
{...study}
|
||||
{...thumb}
|
||||
id={`${studyIndex}_${thumbIndex}`}
|
||||
|
||||
@@ -38,7 +38,6 @@ class ThumbnailEntry extends Component {
|
||||
let className = classnames('ThumbnailEntry noselect', {
|
||||
active: this.props.active,
|
||||
});
|
||||
const infoOnly = false;
|
||||
|
||||
let contents = null;
|
||||
if (this.props.imageSrc || this.props.imageId) {
|
||||
@@ -54,8 +53,10 @@ class ThumbnailEntry extends Component {
|
||||
);
|
||||
} else if (this.props.altImageText) {
|
||||
contents = (
|
||||
<div className={'alt-image-text p-x-1'}>
|
||||
<h1>{this.props.altImageText}</h1>
|
||||
<div className="p-x-1">
|
||||
<div className="alt-image-text">
|
||||
<h1>{this.props.altImageText}</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -69,23 +70,23 @@ class ThumbnailEntry extends Component {
|
||||
>
|
||||
{contents}
|
||||
<div
|
||||
className={infoOnly ? 'series-details info-only' : 'series-details'}
|
||||
className="series-details"
|
||||
>
|
||||
<div className="series-description">
|
||||
{this.props.seriesDescription}
|
||||
</div>
|
||||
<div className="series-information">
|
||||
<div className="item item-series clearfix">
|
||||
<div className="item item-series">
|
||||
<div className="icon">S:</div>
|
||||
<div className="value">{this.props.seriesNumber}</div>
|
||||
</div>
|
||||
{hasInstanceNumber && (
|
||||
<div className="item item-series clearfix">
|
||||
<div className="item item-series">
|
||||
<div className="icon">I:</div>
|
||||
<div className="value">{this.props.instanceNumber}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="item item-frames clearfix">
|
||||
<div className="item item-frames">
|
||||
<div className="icon">
|
||||
<div />
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,6 @@
|
||||
color: var(--text-primary-color);
|
||||
|
||||
.series-details
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 14px
|
||||
line-height: 1.3em
|
||||
@@ -44,9 +42,6 @@
|
||||
position: relative
|
||||
word-wrap: break-word
|
||||
|
||||
.series-description
|
||||
flex-grow: 1
|
||||
|
||||
&.info-only
|
||||
.series-description
|
||||
display: none
|
||||
@@ -67,36 +62,33 @@
|
||||
float: none
|
||||
line-height: 25px
|
||||
|
||||
.value
|
||||
margin-left: 0
|
||||
width: auto
|
||||
|
||||
.series-information
|
||||
padding-right: 4px
|
||||
max-width: 50px
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
|
||||
.item
|
||||
flex: 1
|
||||
text-align: center
|
||||
|
||||
.item-frames .icon
|
||||
height: 18px
|
||||
|
||||
.value
|
||||
color: var(--text-secondary-color);
|
||||
display: inline-block
|
||||
float: right
|
||||
display: inline
|
||||
font-size: 12px
|
||||
margin-left: 4px
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
width: calc(100% - 15px)
|
||||
|
||||
.icon
|
||||
color: var(--active-color);
|
||||
display: inline-block
|
||||
float: left
|
||||
display: inline
|
||||
font-size: 10px
|
||||
font-weight: 900
|
||||
text-align: right
|
||||
width: 11px
|
||||
|
||||
div
|
||||
background-color: var(--series-count-background-color);
|
||||
|
||||
@@ -45,7 +45,7 @@ class ThumbnailEntryDragSource extends Component {
|
||||
const dropEffect = 'copy';
|
||||
|
||||
return connectDragSource(
|
||||
<div className="ThumbnailEntryContainer">
|
||||
<div className="ThumbnailEntryContainer" data-cy="thumbnail-list">
|
||||
<DragPreview {...this.props} />
|
||||
<ThumbnailEntry {...this.props} />
|
||||
</div>,
|
||||
|
||||
@@ -3,6 +3,79 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.5.0...@ohif/viewer@1.5.1) (2019-10-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 set current viewport as active when switching layouts ([#1018](https://github.com/OHIF/Viewers/issues/1018)) ([2a74355](https://github.com/OHIF/Viewers/commit/2a74355))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.4.5...@ohif/viewer@1.5.0) (2019-10-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Multiple fixes and implementation changes to react-cornerstone-viewport ([1cc94f3](https://github.com/OHIF/Viewers/commit/1cc94f3))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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,50 +1,48 @@
|
||||
/*
|
||||
TODO: Temporarily commented out because it is failing on CI due to timing issues
|
||||
describe('OHIF Study Viewer Page', () => {
|
||||
|
||||
before(() => {
|
||||
cy.openStudy("MISTER^MR");
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', ()=> {
|
||||
cy.get('.ThumbnailEntryContainer')
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.waitDicomImage();
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.its('length')
|
||||
.should('be.gt', 1);
|
||||
});
|
||||
|
||||
it('drags and drop a series thumbnail into viewport', () => {
|
||||
cy.get('.ThumbnailEntryContainer:nth-child(2)') //element to be dragged
|
||||
.drag('.cornerstone-canvas'); //dropzone element
|
||||
|
||||
const overlaySeriesInformation = 'div.ViewportOverlay > div.bottom-left.overlay-element > div';
|
||||
const expectedText = 'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
|
||||
|
||||
cy.get(overlaySeriesInformation)
|
||||
.should('have.text', expectedText);
|
||||
});
|
||||
|
||||
it('checks if Series left panel can be hidden/displayed', ()=> {
|
||||
const seriesButton = '.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton';
|
||||
const leftPanel = 'section.sidepanel.from-left';
|
||||
|
||||
cy.get(seriesButton).click();
|
||||
cy.get(leftPanel).should('not.be.enabled')
|
||||
|
||||
cy.get(seriesButton).click();
|
||||
cy.get(leftPanel).should('be.visible');
|
||||
});
|
||||
|
||||
it('checks if Measurements right panel can be hidden/displayed', ()=> {
|
||||
const measurementsButton = '.pull-right > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton';
|
||||
const rightPanel = 'section.sidepanel.from-right';
|
||||
|
||||
cy.get(measurementsButton).click();
|
||||
cy.get(rightPanel).should('be.visible');
|
||||
|
||||
|
||||
cy.get(measurementsButton).click();
|
||||
cy.get(rightPanel).should('not.be.enabled');
|
||||
});
|
||||
|
||||
});
|
||||
*/
|
||||
|
||||
it('drags and drop a series thumbnail into viewport', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]:nth-child(2)') //element to be dragged
|
||||
.drag('.cornerstone-canvas'); //dropzone element
|
||||
|
||||
const overlaySeriesInformation =
|
||||
'div.ViewportOverlay > div.bottom-left.overlay-element > div';
|
||||
const expectedText =
|
||||
'Ser: 2Img: 1 1/13512 x 512Loc: -17.60 mm Thick: 3.00 mm';
|
||||
|
||||
cy.get(overlaySeriesInformation).should('have.text', expectedText);
|
||||
});
|
||||
|
||||
it('checks if Series left panel can be hidden/displayed', () => {
|
||||
const seriesButton =
|
||||
'.pull-left > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton';
|
||||
const leftPanel = 'section.sidepanel.from-left';
|
||||
|
||||
cy.get(seriesButton).click();
|
||||
cy.get(leftPanel).should('not.be.enabled');
|
||||
|
||||
cy.get(seriesButton).click();
|
||||
cy.get(leftPanel).should('be.visible');
|
||||
});
|
||||
|
||||
it('checks if Measurements right panel can be hidden/displayed', () => {
|
||||
const measurementsButton =
|
||||
'.pull-right > .RoundedButtonGroup > .roundedButtonWrapper > .roundedButton';
|
||||
const rightPanel = 'section.sidepanel.from-right';
|
||||
|
||||
cy.get(measurementsButton).click();
|
||||
cy.get(rightPanel).should('be.visible');
|
||||
|
||||
cy.get(measurementsButton).click();
|
||||
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
|
||||
@@ -26,16 +28,118 @@ import { DragSimulator } from "../helpers/DragSimulator.js";
|
||||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add('openStudy', (patientName) => {
|
||||
cy.visit('/');
|
||||
cy.get('#patientName')
|
||||
.type(patientName);
|
||||
cy.get('.studylistStudy > .patientName')
|
||||
.contains(patientName)
|
||||
.click();
|
||||
Cypress.Commands.add('openStudy', patientName => {
|
||||
cy.visit('/');
|
||||
cy.get('#patientName').type(patientName);
|
||||
|
||||
cy.get('.studylistStudy > .patientName')
|
||||
.as('patientResult')
|
||||
.then({ timeout: 5000 }, $patientResult => {
|
||||
cy.contains(patientName).click();
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('drag', { prevSubject: 'element' }, (...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 });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('drag', {prevSubject: 'element',},
|
||||
(...args) => DragSimulator.simulate(...args)
|
||||
);
|
||||
//Command to wait DICOM image to load into the viewport
|
||||
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
|
||||
cy.window()
|
||||
.its('cornerstone')
|
||||
.then({ timeout }, $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",
|
||||
"version": "1.3.3",
|
||||
"version": "1.5.1",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -46,14 +46,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^1.0.2",
|
||||
"@ohif/core": "^1.2.0",
|
||||
"@ohif/extension-cornerstone": "^2.0.0",
|
||||
"@ohif/extension-dicom-html": "^1.0.0",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||
"@ohif/extension-vtk": "^0.51.2",
|
||||
"@ohif/i18n": "^0.50.4",
|
||||
"@ohif/ui": "^0.52.0",
|
||||
"@ohif/extension-vtk": "^0.52.3",
|
||||
"@ohif/i18n": "^0.50.5",
|
||||
"@ohif/ui": "^0.53.2",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
|
||||
@@ -13,6 +13,7 @@ window.config = {
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ window.config = {
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ window.config = {
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from './utils/index.js';
|
||||
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
|
||||
// ~~ EXTENSIONS
|
||||
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
|
||||
@@ -27,7 +26,6 @@ import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { getActiveContexts } from './store/layout/selectors.js';
|
||||
import i18n from '@ohif/i18n';
|
||||
import setupTools from './setupTools.js';
|
||||
import store from './store';
|
||||
|
||||
// Contexts
|
||||
@@ -36,11 +34,6 @@ import UserManagerContext from './context/UserManagerContext';
|
||||
import AppContext from './context/AppContext';
|
||||
|
||||
// ~~~~ APP SETUP
|
||||
initCornerstoneTools({
|
||||
globalToolSyncEnabled: true,
|
||||
showSVGCursors: true,
|
||||
});
|
||||
|
||||
const commandsManagerConfig = {
|
||||
getAppState: () => store.getState(),
|
||||
getActiveContexts: () => getActiveContexts(store.getState()),
|
||||
@@ -49,9 +42,6 @@ const commandsManagerConfig = {
|
||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||
const hotkeysManager = new HotkeysManager(commandsManager);
|
||||
const extensionManager = new ExtensionManager({ commandsManager });
|
||||
|
||||
// CornerstoneTools and labeling/measurements?
|
||||
setupTools(store);
|
||||
// ~~~~ END APP SETUP
|
||||
|
||||
// TODO[react] Use a provider when the whole tree is React
|
||||
@@ -175,8 +165,9 @@ class App extends Component {
|
||||
function _initExtensions(extensions, hotkeys) {
|
||||
const defaultExtensions = [
|
||||
GenericViewerCommands,
|
||||
MeasurementsPanel,
|
||||
OHIFCornerstoneExtension,
|
||||
// WARNING: MUST BE REGISTERED _AFTER_ OHIFCORNERSTONEEXTENSION
|
||||
MeasurementsPanel,
|
||||
];
|
||||
const mergedExtensions = defaultExtensions.concat(extensions);
|
||||
extensionManager.registerExtensions(mergedExtensions);
|
||||
|
||||
@@ -6,7 +6,7 @@ import cornerstone from 'cornerstone-core';
|
||||
|
||||
//
|
||||
import jumpToRowItem from './jumpToRowItem.js';
|
||||
import getMeasurementLocationCallback from './../../lib/getMeasurementLocationCallback';
|
||||
import getMeasurementLocationCallback from './getMeasurementLocationCallback';
|
||||
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
const { MeasurementApi } = OHIF.measurements;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const setLabellingFlowDataAction = labellingFlowData => ({
|
||||
type: 'SET_LABELLING_FLOW_DATA',
|
||||
labellingFlowData,
|
||||
});
|
||||
|
||||
const resetLabellingAndContextMenuAction = state => ({
|
||||
type: 'RESET_LABELLING_AND_CONTEXT_MENU',
|
||||
state,
|
||||
});
|
||||
|
||||
const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({
|
||||
type: 'SET_TOOL_CONTEXT_MENU_DATA',
|
||||
viewportIndex,
|
||||
toolContextMenuData,
|
||||
});
|
||||
|
||||
export {
|
||||
resetLabellingAndContextMenuAction,
|
||||
setLabellingFlowDataAction,
|
||||
setToolContextMenuDataAction,
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData';
|
||||
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
|
||||
|
||||
export default function getMeasurementLocationCallback(
|
||||
eventData,
|
||||
@@ -1,7 +1,15 @@
|
||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
|
||||
import init from './init.js';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'measurements-table',
|
||||
|
||||
preRegistration(configuration = {}) {
|
||||
init(configuration);
|
||||
},
|
||||
getPanelModule() {
|
||||
return {
|
||||
menuOptions: [
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import {
|
||||
getToolLabellingFlowCallback,
|
||||
getOnRightClickCallback,
|
||||
getOnTouchPressCallback,
|
||||
getResetLabellingAndContextMenu,
|
||||
} from './labelingFlowCallbacks.js';
|
||||
import throttle from 'lodash.throttle';
|
||||
|
||||
// TODO: This only works because we have a hard dependency on this extension
|
||||
// We need to decouple and make stuff like this possible w/o bundling this at
|
||||
// build time
|
||||
import store from './../../store';
|
||||
|
||||
const {
|
||||
onAdded,
|
||||
onRemoved,
|
||||
onModified,
|
||||
} = OHIF.measurements.MeasurementHandlers;
|
||||
|
||||
const MEASUREMENT_ACTION_MAP = {
|
||||
added: onAdded,
|
||||
removed: onRemoved,
|
||||
modified: throttle(event => {
|
||||
return onModified(event);
|
||||
}, 300),
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @param {*} configuration
|
||||
*/
|
||||
export default function init(configuration) {
|
||||
// If these tools were already added by a different extension, we want to replace
|
||||
// them with the same tools that have an alternative configuration. By passing in
|
||||
// our custom `getMeasurementLocationCallback`, we can...
|
||||
const toolLabellingFlowCallback = getToolLabellingFlowCallback(store);
|
||||
|
||||
// Removes all tools from all enabled elements w/ provided name
|
||||
// Not commonly used API, so :eyes: for unknown side-effects
|
||||
csTools.removeTool('Bidirectional');
|
||||
csTools.removeTool('Length');
|
||||
csTools.removeTool('Angle');
|
||||
csTools.removeTool('FreehandRoi');
|
||||
csTools.removeTool('EllipticalRoi');
|
||||
csTools.removeTool('CircleRoi');
|
||||
csTools.removeTool('RectangleRoi');
|
||||
csTools.removeTool('ArrowAnnotate');
|
||||
|
||||
// Re-add each tool w/ our custom configuration
|
||||
csTools.addTool(csTools.BidirectionalTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.LengthTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.AngleTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.FreehandRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.EllipticalRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.CircleRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.RectangleRoiTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
|
||||
const onMeasurementsChanged = (action, event) => {
|
||||
return MEASUREMENT_ACTION_MAP[action](event);
|
||||
};
|
||||
const onMeasurementAdded = onMeasurementsChanged.bind(this, 'added');
|
||||
const onMeasurementRemoved = onMeasurementsChanged.bind(this, 'removed');
|
||||
const onMeasurementModified = onMeasurementsChanged.bind(this, 'modified');
|
||||
const onLabelmapModified = onMeasurementsChanged.bind(
|
||||
this,
|
||||
'labelmapModified'
|
||||
);
|
||||
//
|
||||
const onRightClick = getOnRightClickCallback(store);
|
||||
const onTouchPress = getOnTouchPressCallback(store);
|
||||
const onNewImage = getResetLabellingAndContextMenu(store);
|
||||
const onMouseClick = getResetLabellingAndContextMenu(store);
|
||||
const onTouchStart = getResetLabellingAndContextMenu(store);
|
||||
|
||||
// Because click gives us the native "mouse up", buttons will always be `0`
|
||||
// Need to fallback to event.which;
|
||||
const handleClick = cornerstoneMouseClickEvent => {
|
||||
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
|
||||
const isRightClick = mouseUpEvent.which === 3;
|
||||
|
||||
if (isRightClick) {
|
||||
onRightClick(cornerstoneMouseClickEvent);
|
||||
} else {
|
||||
onMouseClick(cornerstoneMouseClickEvent);
|
||||
}
|
||||
};
|
||||
|
||||
function elementEnabledHandler(evt) {
|
||||
const element = evt.detail.element;
|
||||
|
||||
element.addEventListener(
|
||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
||||
onMeasurementAdded
|
||||
);
|
||||
element.addEventListener(
|
||||
csTools.EVENTS.MEASUREMENT_REMOVED,
|
||||
onMeasurementRemoved
|
||||
);
|
||||
element.addEventListener(
|
||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
||||
onMeasurementModified
|
||||
);
|
||||
element.addEventListener(
|
||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
||||
onLabelmapModified
|
||||
);
|
||||
//
|
||||
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
||||
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
||||
// TODO: This makes scrolling painfully slow
|
||||
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
||||
}
|
||||
|
||||
function elementDisabledHandler(evt) {
|
||||
const element = evt.detail.element;
|
||||
|
||||
element.removeEventListener(
|
||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
||||
onMeasurementAdded
|
||||
);
|
||||
element.removeEventListener(
|
||||
csTools.EVENTS.MEASUREMENT_REMOVED,
|
||||
onMeasurementRemoved
|
||||
);
|
||||
element.removeEventListener(
|
||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
||||
onMeasurementModified
|
||||
);
|
||||
element.removeEventListener(
|
||||
csTools.EVENTS.LABELMAP_MODIFIED,
|
||||
onLabelmapModified
|
||||
);
|
||||
//
|
||||
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
|
||||
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
|
||||
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
|
||||
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
|
||||
}
|
||||
|
||||
cornerstone.events.addEventListener(
|
||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
||||
elementEnabledHandler
|
||||
);
|
||||
cornerstone.events.addEventListener(
|
||||
cornerstone.EVENTS.ELEMENT_DISABLED,
|
||||
elementDisabledHandler
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
resetLabellingAndContextMenuAction,
|
||||
setToolContextMenuDataAction,
|
||||
setLabellingFlowDataAction,
|
||||
} from './actions.js';
|
||||
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
|
||||
|
||||
const VIEWPORT_INDEX = 0;
|
||||
|
||||
function getOnRightClickCallback(store) {
|
||||
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
|
||||
const getOnCloseCallback = viewportIndex => {
|
||||
return function onClose() {
|
||||
const toolContextMenuData = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return function onRightClick(event) {
|
||||
const eventData = event.detail;
|
||||
const viewportIndex = VIEWPORT_INDEX; // parseInt(eventData.element.dataset.viewportIndex, 10);
|
||||
|
||||
const toolContextMenuData = {
|
||||
eventData,
|
||||
isTouchEvent: false,
|
||||
onClose: getOnCloseCallback(viewportIndex),
|
||||
};
|
||||
|
||||
// setToolContextMenuData(viewportIndex, toolContextMenuData);
|
||||
setToolContextMenuData(0, toolContextMenuData);
|
||||
};
|
||||
}
|
||||
|
||||
function getOnTouchPressCallback(store) {
|
||||
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
|
||||
const getOnCloseCallback = viewportIndex => {
|
||||
return function onClose() {
|
||||
const toolContextMenuData = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return function onTouchPress(event) {
|
||||
const eventData = event.detail;
|
||||
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10);
|
||||
|
||||
const toolContextMenuData = {
|
||||
eventData,
|
||||
isTouchEvent: true,
|
||||
onClose: getOnCloseCallback(viewportIndex),
|
||||
};
|
||||
|
||||
setToolContextMenuData(viewportIndex, toolContextMenuData);
|
||||
};
|
||||
}
|
||||
|
||||
function getResetLabellingAndContextMenu(store) {
|
||||
return function resetLabellingAndContextMenu() {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} store
|
||||
* @returns
|
||||
*/
|
||||
function getToolLabellingFlowCallback(store) {
|
||||
const setLabellingFlowData = labellingFlowData => {
|
||||
store.dispatch(setLabellingFlowDataAction(labellingFlowData));
|
||||
};
|
||||
|
||||
return function toolLabellingFlowCallback(
|
||||
measurementData,
|
||||
eventData,
|
||||
doneCallback,
|
||||
options = {}
|
||||
) {
|
||||
const updateLabelling = ({ location, response, description }) => {
|
||||
// Update the measurement data with the labelling parameters
|
||||
|
||||
if (location) {
|
||||
measurementData.location = location;
|
||||
}
|
||||
if (description) {
|
||||
measurementData.description = description;
|
||||
}
|
||||
if (response) {
|
||||
measurementData.response = response;
|
||||
}
|
||||
|
||||
updateTableWithNewMeasurementData(measurementData);
|
||||
};
|
||||
|
||||
const labellingDoneCallback = () => {
|
||||
setLabellingFlowData({ visible: false });
|
||||
};
|
||||
|
||||
const labellingFlowData = {
|
||||
visible: true,
|
||||
eventData,
|
||||
measurementData,
|
||||
skipAddLabelButton: options.skipAddLabelButton,
|
||||
editLocation: options.editLocation,
|
||||
editDescription: options.editDescription,
|
||||
editResponse: options.editResponse,
|
||||
editDescriptionOnDialog: options.editDescriptionOnDialog,
|
||||
labellingDoneCallback,
|
||||
updateLabelling,
|
||||
};
|
||||
|
||||
setLabellingFlowData(labellingFlowData);
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
getToolLabellingFlowCallback,
|
||||
getOnRightClickCallback,
|
||||
getOnTouchPressCallback,
|
||||
getResetLabellingAndContextMenu,
|
||||
};
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import OHIF from "@ohif/core";
|
||||
import cornerstone from "cornerstone-core";
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
export default function updateTableWithNewMeasurementData({
|
||||
toolType,
|
||||
measurementNumber,
|
||||
location,
|
||||
description
|
||||
description,
|
||||
}) {
|
||||
// Update all measurements by measurement number
|
||||
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||
@@ -2,18 +2,19 @@ import { LayoutButton } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const { setLayout } = OHIF.redux.actions;
|
||||
const { setLayout, setViewportActive } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
currentLayout: state.viewports.layout,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
// TODO: Change if layout switched becomes more complex
|
||||
onChange: (selectedCell, currentLayout) => {
|
||||
onChange: (selectedCell, currentLayout, activeViewportIndex) => {
|
||||
let viewports = [];
|
||||
const rows = selectedCell.row + 1;
|
||||
const columns = selectedCell.col + 1;
|
||||
@@ -36,6 +37,11 @@ const mapDispatchToProps = dispatch => {
|
||||
viewports,
|
||||
};
|
||||
|
||||
const maxActiveIndex = rows * columns - 1;
|
||||
if (activeViewportIndex > maxActiveIndex) {
|
||||
dispatch(setViewportActive(0));
|
||||
}
|
||||
|
||||
dispatch(setLayout(layout));
|
||||
},
|
||||
};
|
||||
@@ -43,10 +49,10 @@ const mapDispatchToProps = dispatch => {
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch) => {
|
||||
const onChangeFromDispatch = propsFromDispatch.onChange;
|
||||
const { currentLayout } = propsFromState;
|
||||
const { currentLayout, activeViewportIndex } = propsFromState;
|
||||
|
||||
return {
|
||||
onChange: (selectedCell) => onChangeFromDispatch(selectedCell, currentLayout)
|
||||
onChange: selectedCell => onChangeFromDispatch(selectedCell, currentLayout, activeViewportIndex)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,9 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const { viewportIndex } = ownProps;
|
||||
const toolContextMenuData = state.ui.contextMenu[viewportIndex];
|
||||
let availableTools;
|
||||
|
||||
if (
|
||||
state.extensions &&
|
||||
state.extensions.cornerstone &&
|
||||
state.extensions.cornerstone.availableTools
|
||||
) {
|
||||
availableTools = state.extensions.cornerstone.availableTools;
|
||||
}
|
||||
|
||||
return {
|
||||
...toolContextMenuData,
|
||||
availableTools,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback';
|
||||
// This whole component should live in the Measurements Extension :thinking:
|
||||
import getMeasurementLocationCallback from '../appExtensions/MeasurementsPanel/getMeasurementLocationCallback';
|
||||
|
||||
import './ToolContextMenu.css';
|
||||
|
||||
@@ -97,12 +98,11 @@ function getNearbyToolData(element, coords, toolTypes) {
|
||||
return pointNearTool ? nearbyTool : undefined;
|
||||
}
|
||||
|
||||
function getDropdownItems(eventData, isTouchEvent = false, availableTools) {
|
||||
function getDropdownItems(eventData, isTouchEvent = false) {
|
||||
const nearbyToolData = getNearbyToolData(
|
||||
eventData.element,
|
||||
eventData.currentPoints.canvas,
|
||||
toolTypes,
|
||||
availableTools
|
||||
toolTypes
|
||||
);
|
||||
|
||||
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
|
||||
@@ -148,7 +148,6 @@ class ToolContextMenu extends Component {
|
||||
isTouchEvent: PropTypes.bool.isRequired,
|
||||
eventData: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
availableTools: PropTypes.array,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -168,12 +167,8 @@ class ToolContextMenu extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isTouchEvent, eventData, availableTools } = this.props;
|
||||
const dropdownItems = getDropdownItems(
|
||||
eventData,
|
||||
isTouchEvent,
|
||||
availableTools
|
||||
);
|
||||
const { isTouchEvent, eventData } = this.props;
|
||||
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
|
||||
|
||||
// Skip if there is no dropdown item
|
||||
if (!dropdownItems.length) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { Component } from "react";
|
||||
import { metadata, studies, utils } from "@ohif/core";
|
||||
import React, { Component } from 'react';
|
||||
import { metadata, studies, utils, log } from '@ohif/core';
|
||||
|
||||
import ConnectedViewer from "./ConnectedViewer.js";
|
||||
import PropTypes from "prop-types";
|
||||
import { extensionManager } from "./../App.js";
|
||||
import ConnectedViewer from './ConnectedViewer.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { extensionManager } from './../App.js';
|
||||
|
||||
const { OHIFStudyMetadata } = metadata;
|
||||
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
||||
const { retrieveStudiesMetadata } = studies;
|
||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
||||
|
||||
@@ -13,65 +13,97 @@ class ViewerRetrieveStudyData extends Component {
|
||||
static propTypes = {
|
||||
studyInstanceUids: PropTypes.array.isRequired,
|
||||
seriesInstanceUids: PropTypes.array,
|
||||
server: PropTypes.object
|
||||
server: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
studies: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
// TODO: Avoid using timepoints here
|
||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
||||
const { studyInstanceUids, seriesInstanceUids, server } = this.props;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
studies: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
async loadStudies() {
|
||||
try {
|
||||
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
|
||||
const studies = await retrieveStudiesMetadata(
|
||||
server,
|
||||
studyInstanceUids,
|
||||
seriesInstanceUids
|
||||
);
|
||||
this.setStudies(studies);
|
||||
} catch (e) {
|
||||
this.setState({ error: true });
|
||||
log.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the viewer when the data is ready
|
||||
// TODO: CLEAR THIS SOMEWHERE ELSE
|
||||
studyMetadataManager.purge();
|
||||
|
||||
setStudies(givenStudies) {
|
||||
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
// Map studies to new format, update metadata manager?
|
||||
const updatedStudies = studies.map(study => {
|
||||
const studies = givenStudies.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.studyInstanceUid
|
||||
);
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules["sopClassHandlerModule"];
|
||||
|
||||
study.displaySets =
|
||||
study.displaySets ||
|
||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
if (!study.displaySets) {
|
||||
study.displaySets = studyMetadata.createDisplaySets(
|
||||
sopClassHandlerModules
|
||||
);
|
||||
}
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(study);
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
|
||||
// Attempt to load remaning series if any
|
||||
this._attemptToLoadRemainingSeries(studyMetadata);
|
||||
return study;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
studies: updatedStudies
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: true
|
||||
});
|
||||
|
||||
// TODO: Handle gracefully instead of throwing?
|
||||
throw new Error(err);
|
||||
this.setState({ studies });
|
||||
}
|
||||
}
|
||||
|
||||
_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() {
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import updateTableWithNewMeasurementData from './lib/updateTableWithNewMeasurementData';
|
||||
|
||||
function getToolLabellingFlowCallback(store) {
|
||||
const setLabellingFlowDataAction = labellingFlowData => ({
|
||||
type: 'SET_LABELLING_FLOW_DATA',
|
||||
labellingFlowData,
|
||||
});
|
||||
|
||||
const setLabellingFlowData = labellingFlowData => {
|
||||
store.dispatch(setLabellingFlowDataAction(labellingFlowData));
|
||||
};
|
||||
|
||||
return function toolLabellingFlowCallback(
|
||||
measurementData,
|
||||
eventData,
|
||||
doneCallback,
|
||||
options = {}
|
||||
) {
|
||||
const updateLabelling = ({ location, response, description }) => {
|
||||
// Update the measurement data with the labelling parameters
|
||||
|
||||
if (location) {
|
||||
measurementData.location = location;
|
||||
}
|
||||
if (description) {
|
||||
measurementData.description = description;
|
||||
}
|
||||
if (response) {
|
||||
measurementData.response = response;
|
||||
}
|
||||
|
||||
updateTableWithNewMeasurementData(measurementData);
|
||||
};
|
||||
|
||||
const labellingDoneCallback = () => {
|
||||
setLabellingFlowData({ visible: false });
|
||||
};
|
||||
|
||||
const labellingFlowData = {
|
||||
visible: true,
|
||||
eventData,
|
||||
measurementData,
|
||||
skipAddLabelButton: options.skipAddLabelButton,
|
||||
editLocation: options.editLocation,
|
||||
editDescription: options.editDescription,
|
||||
editResponse: options.editResponse,
|
||||
editDescriptionOnDialog: options.editDescriptionOnDialog,
|
||||
labellingDoneCallback,
|
||||
updateLabelling,
|
||||
};
|
||||
|
||||
setLabellingFlowData(labellingFlowData);
|
||||
};
|
||||
}
|
||||
|
||||
const resetLabellingAndContextMenuAction = state => ({
|
||||
type: 'RESET_LABELLING_AND_CONTEXT_MENU',
|
||||
state,
|
||||
});
|
||||
|
||||
const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({
|
||||
type: 'SET_TOOL_CONTEXT_MENU_DATA',
|
||||
viewportIndex,
|
||||
toolContextMenuData,
|
||||
});
|
||||
|
||||
function getOnRightClickCallback(store) {
|
||||
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
|
||||
const getOnCloseCallback = viewportIndex => {
|
||||
return function onClose() {
|
||||
const toolContextMenuData = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return function onRightClick(event) {
|
||||
const eventData = event.detail;
|
||||
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10);
|
||||
|
||||
const toolContextMenuData = {
|
||||
eventData,
|
||||
isTouchEvent: false,
|
||||
onClose: getOnCloseCallback(viewportIndex),
|
||||
};
|
||||
|
||||
setToolContextMenuData(viewportIndex, toolContextMenuData);
|
||||
};
|
||||
}
|
||||
|
||||
function getOnTouchPressCallback(store) {
|
||||
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
|
||||
const getOnCloseCallback = viewportIndex => {
|
||||
return function onClose() {
|
||||
const toolContextMenuData = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
store.dispatch(
|
||||
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
return function onTouchPress(event) {
|
||||
const eventData = event.detail;
|
||||
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10);
|
||||
|
||||
const toolContextMenuData = {
|
||||
eventData,
|
||||
isTouchEvent: true,
|
||||
onClose: getOnCloseCallback(viewportIndex),
|
||||
};
|
||||
|
||||
setToolContextMenuData(viewportIndex, toolContextMenuData);
|
||||
};
|
||||
}
|
||||
|
||||
function getResetLabellingAndContextMenu(store) {
|
||||
return function resetLabellingAndContextMenu() {
|
||||
store.dispatch(resetLabellingAndContextMenuAction());
|
||||
};
|
||||
}
|
||||
|
||||
export default function setupTools(store) {
|
||||
const toolLabellingFlowCallback = getToolLabellingFlowCallback(store);
|
||||
const availableTools = [
|
||||
{ name: 'Pan', mouseButtonMasks: [1, 4] },
|
||||
{ name: 'Zoom', mouseButtonMasks: [1, 2] },
|
||||
{ name: 'Wwwc', mouseButtonMasks: [1] },
|
||||
{ name: 'Magnify' },
|
||||
{ name: 'WwwcRegion' },
|
||||
{
|
||||
name: 'Bidirectional',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{
|
||||
name: 'Length',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{
|
||||
name: 'Angle',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{ name: 'StackScroll', mouseButtonMasks: [1] },
|
||||
{ name: 'Brush', mouseButtonMasks: [1] },
|
||||
{
|
||||
name: 'FreehandRoi',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{
|
||||
name: 'EllipticalRoi',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{
|
||||
name: 'CircleRoi',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{
|
||||
name: 'RectangleRoi',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
mouseButtonMasks: [1],
|
||||
},
|
||||
{ name: 'DragProbe' },
|
||||
{ name: 'PanMultiTouch' },
|
||||
{ name: 'ZoomTouchPinch' },
|
||||
{ name: 'StackScrollMouseWheel' },
|
||||
{ name: 'StackScrollMultiTouch' },
|
||||
{ name: 'Eraser' },
|
||||
{
|
||||
name: 'ArrowAnnotate',
|
||||
props: {
|
||||
configuration: {
|
||||
getMeasurementLocationCallback: toolLabellingFlowCallback,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const onRightClick = getOnRightClickCallback(store);
|
||||
const onTouchPress = getOnTouchPressCallback(store);
|
||||
const onNewImage = getResetLabellingAndContextMenu(store);
|
||||
const onMouseClick = getResetLabellingAndContextMenu(store);
|
||||
const onTouchStart = getResetLabellingAndContextMenu(store);
|
||||
const toolAction = OHIF.redux.actions.setExtensionData('cornerstone', {
|
||||
availableTools,
|
||||
onNewImage,
|
||||
onRightClick,
|
||||
onTouchPress,
|
||||
onTouchStart,
|
||||
onMouseClick,
|
||||
});
|
||||
|
||||
store.dispatch(toolAction);
|
||||
}
|
||||
Reference in new issue
Block a user