Compare commits

...
Author SHA1 Message Date
ohif-bot 2e5a385e53 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.5
 - @ohif/core@2.3.2
 - @ohif/viewer@3.3.5
2020-01-06 21:29:37 +00:00
Danny Brown a3caeb381e chore: remove unused file; duplicated by studyInstanceHelpers (#1349)
* chore: remove unused file; duplicated by studyInstanceHelpers

* Change expected study list results num
2020-01-06 16:26:57 -05:00
ohif-bot be995f93b6 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.4
 - @ohif/core@2.3.1
 - @ohif/viewer@3.3.4
2019-12-30 20:01:54 +00:00
Danny Brown 5036615e00 chore: throw our error if we encounter one (no swallow) (#1337) 2019-12-30 14:53:39 -05:00
Gustavo André Lelis 2e215aed4c test: 💍 Updating tests after bug fix (#1324)
Closes: #1167
2019-12-20 19:14:07 -05:00
ohif-bot ffd27d6db4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.1
 - @ohif/extension-vtk@1.2.3
 - @ohif/viewer@3.3.3
2019-12-20 21:01:26 +00:00
Gustavo André LelisandDanny Brown 6540e36818 fix: 🐛 1241: Make Plugin switch part of ToolbarModule (#1322)
* fix: 🐛 Make Plugin switch part of ToolbarModule

Closes: #1241

* Move setCornerstoneViewport command into cornerstone extension

* Exposing extensionManager into extension's get modules functions

* Remove Plugin switch

* Updating set cornerstonelayout to work on VIEWER context

* Creating a customComponent for 2D MPR button

* Use extensionManager to show or not buttons
Adding isHidden option into toolbarModules

* Make buttons hidden depending on extension registration

* Fix unit test after changes on extensionManager

* Remove workaround for extensionManager

* Use studyMeatadataManager and remove TODO's

* Remove storeContext not needed

* Moving Exit 2D MPR into cornerstone extension with VTK context

* Remove PluginSwitch files

* Fix E2E tests

* Adding cypress screenshots into gitignore

* Remove extensionManager from getModuleFn functions and moving CustomComponent for VTK button to VTK extension

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 15:59:36 -05:00
ohif-bot a436436def chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.2
 - @ohif/ui@1.1.7
 - @ohif/viewer@3.3.2
2019-12-20 19:10:48 +00:00
Mirna Silva d6d92afbda test: Refactoring toolbar data-cy selectors (#1323) 2019-12-20 14:07:00 -05:00
ohif-bot e1142910e4 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.2.1
 - @ohif/ui@1.1.6
 - @ohif/viewer@3.3.1
2019-12-20 18:57:38 +00:00
Mirna Silva 067e6c769b test: Save Measurements Functionality (#1300)
* E2E Tests: Save Measurements Functionality

* Moving test file to PWA only. Script-tag doesn't have the required Study

* Removing data-cy selector from cornerstone toolbar
2019-12-20 13:54:59 -05:00
ohif-bot d375d02a4b chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.2.0
 - @ohif/extension-vtk@1.2.0
 - @ohif/core@2.3.0
 - @ohif/viewer@3.3.0
2019-12-20 17:18:56 +00:00
Igor OctavianoandDanny Brown 63594d36b0 feat: 🎸 Configuration so viewer tools can nix handles (#1304)
* feat: 🎸 Configuration so viewer tools can nix handles

Adds a key to cornerstone configuration that allows to toggle on/off
handle rendering

Closes: #1223

* Update extension docs

* Update doc

* docs: include hideHandles configuration in our configuration docs

* chore: default to hiding handles for annotations

* chore: switch to drawHandlesOnHover to mimic legacy viewer functionality

* Flip logic for drawHandlesOnHover; reverse of hideHandles

* Don't hide the handles of ellipse or rectangle roi

* invert check

Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
2019-12-20 12:15:40 -05:00
ohif-bot 6c160f517f chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.7
 - @ohif/ui@1.1.5
 - @ohif/viewer@3.2.2
2019-12-19 15:15:49 +00:00
Gustavo André Lelis 23305cec9c fix: 🐛 Fix drag-n-drop of local files into OHIF (#1319)
* fix: 🐛 Fix drag-n-drop of local files into OHIF

Closes: #1307

* CR Update: Centralize dialogs
2019-12-19 10:14:05 -05:00
ohif-bot 36a741f0bc chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.6
 - @ohif/core@2.2.1
 - @ohif/viewer@3.2.1
2019-12-18 18:29:28 +00:00
Igor Octaviano 695ad732e1 refactor: 💡 Move DICOMSR code to @ohif/core project (#1310)
* Move DICOMSR to @ohif/core

* refactor: 💡 Move DICOMSR code to @ohif/core project

We've just merged support for DICOM Structured Reports. Today, support
for this feature lives in the @ohif/viewer project in it's lib folder.
Ideally, it should be moved to @ohif/core and given a clear public API
and unit tests.

Closes: #1282

* Update imports

* CR Update: Update imports
2019-12-18 13:25:53 -05:00
ohif-bot 73a3f07e2b chore(release): publish [skip ci]
- @ohif/extension-vtk@1.1.5
 - @ohif/core@2.2.0
 - @ohif/viewer@3.2.0
2019-12-16 19:11:59 +00:00
Igor Octaviano 4ea239a953 feat: 🎸 Expose extension config to modules (#1279)
* feat: 🎸 Expose extension config to modules

Currently, only the preRegistration hook receives the extension's
configuration as a parameter. Providing getModuleFn's with the
extension's configuration, and all lifecycle/modules with the
application's configuration as rootConfig should open the doors to more
configurable extensions.

Closes: #1268

* CR Update: Pass extension and service config through extension manager preinit/getmodule

* CR Update: Remove appConfig from servicesManager

* CR Update: Remove appconfig variable
2019-12-16 14:07:53 -05:00
ohif-bot 625d5d70a7 chore(release): publish [skip ci]
- @ohif/viewer@3.1.12
2019-12-16 18:30:12 +00:00
Igor Octaviano 5c3d8b37b6 fix: 🐛 Dismiss all dialogs if leaving viewer route #1242 (#1301)
* fix: 🐛 #1242

Dismiss all dialogs if route change

Closes: #1242

* CR Update: Dismiss dialogs in videwer
2019-12-16 13:26:31 -05:00
58 changed files with 929 additions and 821 deletions

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
# Cypress test results
videos/
screenshots/
# Locize settings
.locize
+5 -3
View File
@@ -193,7 +193,8 @@ today, create a GitHub issue!
The `ExtensionManager` is a class made available to us via the `@ohif/core`
project (platform/core). Our application instantiates a single instance of it,
and provides a `ServicesManager` and `CommandsManager`.
and provides a `ServicesManager` and `CommandsManager` along with the application's
configuration through the appConfig key (optional).
```js
const commandsManager = new CommandsManager();
@@ -201,6 +202,7 @@ const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig
});
```
@@ -210,8 +212,8 @@ The `ExtensionManager` only has a few public members:
- `registerExtensions` - Registers an array of extensions
- `modules` - An object containing registered extensions by `MODULE_TYPE`
During registration, lifecycle hooks and modules have access to
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
During registration, lifecycle hooks and modules have access to the extension's config,
the application's config and `ExtensionManager`'s `ServicesManager` and `CommandsManager` instances.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
appropriate places in our application.
+14 -3
View File
@@ -1,6 +1,9 @@
# Viewer: Configuration
We maintain a number of common viewer application configurations at [`<root>/platform/viewer/public/configs`][config-dir]. How these values are passed to the viewer depend on how it's deployed, but the two most common paths are:
We maintain a number of common viewer application configurations at
[`<root>/platform/viewer/public/configs`][config-dir]. How these values are
passed to the viewer depend on how it's deployed, but the two most common paths
are:
- `index.html` looks for `https://your-website.com/app-config.js` OR
- `index.html` passes the values to `OHIF.installViewer()`
@@ -42,8 +45,16 @@ window.config = {
keys: ['v'],
},
],
// Config to pass to the bundled cornerstone extension
cornerstoneExtensionConfig: {},
/* Configuration passed to the bundled cornerstone extension
*
* The cornerstone extension is currently tightly coupled to the platform.
* Until we're able to decouple it, this key will serve as a workaround to
* pass it configuration.
*/
cornerstoneExtensionConfig: {
/* Whether to show/hide annotation "handles" */
hideHandles: true,
},
};
```
+1
View File
@@ -14,6 +14,7 @@ export default {
preRegistration({
servicesManager = {},
commandsManager = {},
appConfig = {},
configuration = {},
}) {},
+22
View File
@@ -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.
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
+14
View File
@@ -85,6 +85,20 @@ Tools can be configured through extension configuration using the tools key:
...
```
## Annotate Tools Configuration
*We currently support one property for annotation tools.*
### Hide handles
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
```js
...
cornerstoneExtensionConfig: {
hideHandles: true,
},
...
## Resources
### Repositories
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "2.1.1",
"version": "2.2.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -32,7 +32,7 @@
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
@@ -2,6 +2,7 @@ import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import OHIF from '@ohif/core';
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
import { getEnabledElement } from './state';
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
const scroll = cornerstoneTools.import('util/scroll');
@@ -242,6 +243,9 @@ const commandsModule = ({ servicesManager }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
setCornerstoneLayout: () => {
setCornerstoneLayout();
}
};
const definitions = {
@@ -348,6 +352,12 @@ const commandsModule = ({ servicesManager }) => {
storeContexts: [],
options: {},
},
setCornerstoneLayout: {
commandFn: actions.setCornerstoneLayout,
storeContexts: [],
options: {},
context: 'VIEWER',
},
};
return {
+65 -26
View File
@@ -76,31 +76,35 @@ export default function init({ servicesManager, configuration }) {
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const tools = [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
// Touch
csTools.PanMultiTouchTool,
csTools.ZoomTouchPinchTool,
// Annotations
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
// Segmentation
csTools.BrushTool,
];
const toolsGroupedByType = {
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
annotations: [
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
],
segmentation: [csTools.BrushTool],
other: [
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.WwwcRegionTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
],
};
let tools = [];
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
tools.push(...toolsGroupedByType[toolsGroup])
);
/* Add extension tools configuration here. */
const internalToolsConfig = {
@@ -114,13 +118,48 @@ export default function init({ servicesManager, configuration }) {
},
};
/* Abstract tools configuration using extension configuration. */
const parseToolProps = (props, tool) => {
const { annotations } = toolsGroupedByType;
// An alternative approach would be to remove the `drawHandlesOnHover` config
// from the supported configuration properties in `cornerstone-tools`
const toolsWithHideableHandles = annotations.filter(
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
);
let parsedProps = { ...props };
/**
* drawHandles - Never/Always show handles
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
*
* Does not apply to tools where handles aren't placed in predictable
* locations.
*/
if (
configuration.hideHandles !== false &&
toolsWithHideableHandles.includes(tool)
) {
if (props.configuration) {
parsedProps.configuration.drawHandlesOnHover = true;
} else {
parsedProps.configuration = { drawHandlesOnHover: true };
}
}
return parsedProps;
};
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const externalToolsConfig = configuration.tools || {};
const externalToolProps = externalToolsConfig[toolName] || {};
const internalToolProps = internalToolsConfig[toolName] || {};
const props = merge(internalToolProps, externalToolProps);
const props = merge(
internalToolProps,
parseToolProps(externalToolProps, tool)
);
csTools.addTool(tool, props);
});
@@ -237,6 +237,15 @@ const definitions = [
},
],
},
{
id: 'Exit2DMPR',
label: 'Exit 2D MPR',
icon: 'times',
//
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setCornerstoneLayout',
context: 'ACTIVE_VIEWPORT::VTK',
}
];
export default {
@@ -0,0 +1,22 @@
import { redux } from '@ohif/core';
const { setLayout } = redux.actions;
/**
* Update the current layout with a simple Cornerstone one
*
* @return void
*/
const setCornerstoneLayout = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
const action = setLayout(layout);
window.store.dispatch(action);
}
export default setCornerstoneLayout;
+78
View File
@@ -3,6 +3,84 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.4...@ohif/extension-vtk@1.2.5) (2020-01-06)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.3...@ohif/extension-vtk@1.2.4) (2019-12-30)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.2...@ohif/extension-vtk@1.2.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.1...@ohif/extension-vtk@1.2.2) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.0...@ohif/extension-vtk@1.2.1) (2019-12-20)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.7...@ohif/extension-vtk@1.2.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.6...@ohif/extension-vtk@1.1.7) (2019-12-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.5...@ohif/extension-vtk@1.1.6) (2019-12-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.4...@ohif/extension-vtk@1.1.5) (2019-12-16)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.3...@ohif/extension-vtk@1.1.4) (2019-12-16)
**Note:** Version bump only for package @ohif/extension-vtk
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "1.1.4",
"version": "1.2.5",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -52,9 +52,9 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^2.1.1",
"@ohif/ui": "^1.1.4",
"cornerstone-tools": "^4.8.0",
"@ohif/core": "^2.3.2",
"@ohif/ui": "^1.1.7",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3",
"gh-pages": "^2.0.1",
@@ -0,0 +1,87 @@
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import { utils } from '@ohif/core';
const { studyMetadataManager } = utils;
let isVisible = true;
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
if (!viewportSpecificData[activeViewportIndex]) {
return false;
};
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
activeViewportIndex
];
const studies = studyMetadataManager.all();
const study = studies.find(
study => study.studyInstanceUID === studyInstanceUid
);
if (!study) {
return false;
}
const displaySet = study._displaySets.find(set => set.displaySetInstanceUid === displaySetInstanceUid);
if (!displaySet) {
return false;
};
return displaySet.isReconstructable;
};
function VTKMPRToolbarButton({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const { id, label, icon } = button;
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
const { viewports = {} } = state;
const { viewportSpecificData, activeViewportIndex } = viewports;
return {
viewportSpecificData,
activeViewportIndex,
}
});
isVisible = _isDisplaySetReconstructable(
viewportSpecificData,
activeViewportIndex,
);
return (
<React.Fragment>
{isVisible && (
<ToolbarButton
key={id}
label={label}
icon={icon}
onClick={evt => toolbarClickCallback(button, evt)}
isActive={isActive}
/>
)}
</React.Fragment>
);
}
VTKMPRToolbarButton.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default VTKMPRToolbarButton;
+12 -1
View File
@@ -1,4 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
import VTKMPRToolbarButton from './toolbarComponents/VTKMPRToolbarButton';
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
@@ -115,9 +116,19 @@ const definitions = [
},
],
},
{
id: '2DMPR',
label: '2D MPR',
icon: 'cube',
//
CustomComponent: VTKMPRToolbarButton,
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'mpr2d',
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
},
];
export default {
definitions,
defaultContext: 'ACTIVE_VIEWPORT::VTK',
};
}
+49
View File
@@ -3,6 +3,55 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.1...@ohif/core@2.3.2) (2020-01-06)
**Note:** Version bump only for package @ohif/core
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.0...@ohif/core@2.3.1) (2019-12-30)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.1...@ohif/core@2.3.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.0...@ohif/core@2.2.1) (2019-12-18)
**Note:** Version bump only for package @ohif/core
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.1...@ohif/core@2.2.0) (2019-12-16)
### Features
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.0...@ohif/core@2.1.1) (2019-12-16)
**Note:** Version bump only for package @ohif/core
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "2.1.1",
"version": "2.3.2",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -32,13 +32,14 @@
},
"peerDependencies": {
"cornerstone-core": "^2.2.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dicom-parser": "^1.8.3"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"ajv": "^6.10.0",
"dcmjs": "^0.8.3",
"dicomweb-client": "^0.5.2",
"isomorphic-base64": "^1.0.2",
"lodash.clonedeep": "^4.5.0",
@@ -1,5 +1,10 @@
import { log, studies, utils } from '@ohif/core';
import { retrieveMeasurementFromSR, stowSRFromMeasurements } from './handleStructuredReport';
import log from '../log';
import studies from '../studies';
import utils from '../utils';
import {
retrieveMeasurementFromSR,
stowSRFromMeasurements,
} from './handleStructuredReport';
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
/**
@@ -65,7 +70,9 @@ const storeMeasurements = async (measurementData, filter, server) => {
message: 'Measurements were saved with success',
};
} catch (error) {
log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}`
);
throw new Error('Error while saving the measurements.');
}
};
@@ -1,11 +1,11 @@
import * as dcmjs from 'dcmjs';
import { DICOMWeb } from '@ohif/core';
import { api } from 'dicomweb-client';
import DICOMWeb from '../DICOMWeb';
import parseDicomStructuredReport from './parseDicomStructuredReport';
import parseMeasurementsData from './parseMeasurementsData';
import getAllDisplaySets from './utils/getAllDisplaySets';
import { api } from 'dicomweb-client';
const VERSION_NAME = 'dcmjs-0.0';
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
@@ -34,7 +34,10 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
const displaySets = getAllDisplaySets(studies);
const measurementsData = parseDicomStructuredReport(part10SRArrayBuffer, displaySets);
const measurementsData = parseDicomStructuredReport(
part10SRArrayBuffer,
displaySets
);
return measurementsData;
};
@@ -47,13 +50,10 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
* @returns {Promise}
*/
const stowSRFromMeasurements = async (measurements, serverUrl) => {
const { dataset } = parseMeasurementsData(
measurements
);
const { dataset } = parseMeasurementsData(measurements);
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
const meta = {
FileMetaInformationVersion:
dataset._meta.FileMetaInformationVersion.Value,
FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value,
MediaStorageSOPClassUID: dataset.SOPClassUID,
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
File renamed without changes.
@@ -1,4 +1,5 @@
import * as dcmjs from 'dcmjs';
import findInstanceMetadataBySopInstanceUid from './utils/findInstanceMetadataBySopInstanceUid';
/**
@@ -37,7 +38,12 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
const { studyInstanceUid, patientId } = study;
const { seriesInstanceUid } = series;
const { sopInstanceUid, frameIndex } = measurement;
const imagePath = getImagePath(studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex);
const imagePath = getImagePath(
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frameIndex
);
const imageId = instanceMetadata.getImageId();
if (!imageId) {
@@ -75,8 +81,15 @@ const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
* @param {string} frameIndex
* @returns
*/
const getImagePath = (studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join('_');
}
const getImagePath = (
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frameIndex
) => {
return [studyInstanceUid, seriesInstanceUid, sopInstanceUid, frameIndex].join(
'_'
);
};
export default parseDicomStructuredReport;
@@ -1,6 +1,8 @@
import * as dcmjs from 'dcmjs';
import { log, measurements } from '@ohif/core';
import cornerstone from 'cornerstone-core';
import log from '../log';
import measurements from '../measurements';
import isToolSupported from './utils/isToolSupported';
/**
@@ -11,7 +11,9 @@ const findInstanceMetadataBySopInstanceUid = (displaySets, sopInstanceUid) => {
displaySets.find(displaySet => {
if (!displaySet.images) return false;
instanceFound = displaySet.images.find(instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid);
instanceFound = displaySet.images.find(
instanceMetadata => instanceMetadata._sopInstanceUID === sopInstanceUid
);
return !!instanceFound;
});
@@ -4,15 +4,17 @@
* @param {Array} studies
* @returns {Object} Series
*/
const findMostRecentStructuredReport = (studies) => {
const findMostRecentStructuredReport = studies => {
let mostRecentStructuredReport;
studies.forEach(study => {
const allSeries = study.getSeries ? study.getSeries() : [];
allSeries.forEach(series => {
if (isStructuredReportSeries(series)) {
if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
if (
!mostRecentStructuredReport ||
compareSeriesDate(series, mostRecentStructuredReport)
) {
mostRecentStructuredReport = series;
}
}
@@ -28,7 +30,7 @@ const findMostRecentStructuredReport = (studies) => {
* @param {Object} series - Series metadata
* @returns {boolean}
*/
const isStructuredReportSeries = (series) => {
const isStructuredReportSeries = series => {
const supportedSopClassUIDs = [
'1.2.840.10008.5.1.4.1.1.88.22',
'1.2.840.10008.5.1.4.1.1.11.1',
@@ -38,7 +40,7 @@ const isStructuredReportSeries = (series) => {
const sopClassUid = firstInstance._instance.sopClassUid;
return supportedSopClassUIDs.includes(sopClassUid);
}
};
/**
* Checkes if series1 is newer than series2
@@ -48,10 +50,11 @@ const isStructuredReportSeries = (series) => {
* @returns {boolean} true/false if series1 is newer than series2
*/
const compareSeriesDate = (series1, series2) => {
return series1._data.seriesDate > series2._data.seriesDate ||
return (
series1._data.seriesDate > series2._data.seriesDate ||
(series1._data.seriesDate === series2._data.seriesDate &&
series1._data.seriesTime > series2._data.seriesTime);
}
series1._data.seriesTime > series2._data.seriesTime)
);
};
export default findMostRecentStructuredReport;
@@ -4,7 +4,7 @@
* @param {Object} studies - List of studies loaded into the viewer
* @returns {Object} List of DisplaySets
*/
const getAllDisplaySets = (studies) => {
const getAllDisplaySets = studies => {
let allDisplaySets = [];
studies.forEach(study => {
@@ -2,13 +2,14 @@ import MODULE_TYPES from './MODULE_TYPES.js';
import log from './../log.js';
export default class ExtensionManager {
constructor({ commandsManager, servicesManager }) {
constructor({ commandsManager, servicesManager, appConfig = {} }) {
this.modules = {};
this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES);
//
this._commandsManager = commandsManager;
this._servicesManager = servicesManager;
this._appConfig = appConfig;
this.moduleTypeNames.forEach(moduleType => {
this.modules[moduleType] = [];
@@ -70,6 +71,7 @@ export default class ExtensionManager {
extension.preRegistration({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
});
}
@@ -79,7 +81,8 @@ export default class ExtensionManager {
const extensionModule = this._getExtensionModule(
moduleType,
extension,
extensionId
extensionId,
configuration
);
if (extensionModule) {
@@ -102,7 +105,7 @@ export default class ExtensionManager {
* @param {Object} extension
* @param {string} extensionId - Used for logging warnings
*/
_getExtensionModule(moduleType, extension, extensionId) {
_getExtensionModule(moduleType, extension, extensionId, configuration) {
const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType);
const getModuleFn = extension[getModuleFnName];
@@ -114,6 +117,8 @@ export default class ExtensionManager {
const extensionModule = getModuleFn({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
appConfig: this._appConfig,
configuration,
});
if (!extensionModule) {
@@ -6,7 +6,7 @@ import log from './../log.js';
jest.mock('./../log.js');
describe('ExtensionManager.js', () => {
let extensionManager, commandsManager;
let extensionManager, commandsManager, servicesManager, appConfig;
beforeEach(() => {
commandsManager = {
@@ -14,7 +14,17 @@ describe('ExtensionManager.js', () => {
getContext: jest.fn(),
registerCommand: jest.fn(),
};
extensionManager = new ExtensionManager({ commandsManager });
servicesManager = {
registerService: jest.fn(),
};
appConfig = {
testing: true,
};
extensionManager = new ExtensionManager({
servicesManager,
commandsManager,
appConfig,
});
log.warn.mockClear();
jest.clearAllMocks();
});
@@ -51,7 +61,7 @@ describe('ExtensionManager.js', () => {
extensionManager.registerExtensions(fakeExtensions);
// Assert
expect(extensionManager.registerExtension.mock.calls[1]).toContain(
expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual(
fakeConfiguration
);
});
@@ -67,19 +77,19 @@ describe('ExtensionManager.js', () => {
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
});
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
const configuration = { config: 'Some configuration' };
extensionManager._servicesManager = { services: { TestService: {} } };
it('calls preRegistration() passing dependencies and extension configuration to extension', () => {
const extensionConfiguration = { config: 'Some configuration' };
// SUT
const fakeExtension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(fakeExtension, configuration);
const extension = { one: '1', preRegistration: jest.fn() };
extensionManager.registerExtension(extension, extensionConfiguration);
// Assert
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
configuration,
expect(extension.preRegistration.mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
});
@@ -153,8 +163,8 @@ describe('ExtensionManager.js', () => {
);
});
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
extensionManager._servicesManager = { services: { TestService: {} } };
it('successfully passes dependencies to each module along with extension configuration', () => {
const extensionConfiguration = { testing: true };
const extension = {
id: 'hello-world',
@@ -165,11 +175,17 @@ describe('ExtensionManager.js', () => {
getCommandsModule: jest.fn(),
};
extensionManager.registerExtension(extension);
extensionManager.registerExtension(extension, extensionConfiguration);
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
servicesManager: extensionManager._servicesManager,
commandsManager: extensionManager._commandsManager,
Object.keys(extension).forEach(module => {
if (typeof extension[module] === 'function') {
expect(extension[module].mock.calls[0][0]).toEqual({
servicesManager,
commandsManager,
appConfig,
configuration: extensionConfiguration,
});
}
});
});
@@ -210,7 +226,7 @@ describe('ExtensionManager.js', () => {
return {
definitions: {
exampleDefinition: {
commandFn: () => {},
commandFn: () => { },
storeContexts: [],
options: {},
},
+3
View File
@@ -5,6 +5,7 @@ import { ServicesManager } from './services';
import classes, { CommandsManager, HotkeysManager } from './classes/';
import DICOMWeb from './DICOMWeb';
import DICOMSR from './DICOMSR';
import cornerstone from './cornerstone.js';
import hangingProtocols from './hanging-protocols';
import header from './header.js';
@@ -46,6 +47,7 @@ const OHIF = {
object,
log,
DICOMWeb,
DICOMSR,
viewer: {},
measurements,
hangingProtocols,
@@ -76,6 +78,7 @@ export {
object,
log,
DICOMWeb,
DICOMSR,
measurements,
hangingProtocols,
//
+1
View File
@@ -28,6 +28,7 @@ describe('Top level exports', () => {
'object',
'log',
'DICOMWeb',
'DICOMSR',
'OHIF', //
'measurements',
'hangingProtocols',
@@ -33,7 +33,9 @@ export default class ServicesManager {
}
if (service.create) {
this.services[service.name] = service.create({ configuration });
this.services[service.name] = service.create({
configuration,
});
} else {
log.warn(`Service create factory function not defined. Exiting early.`);
return;
@@ -33,7 +33,7 @@ describe('ServicesManager.js', () => {
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
]);
expect(servicesManager.registerService.mock.calls[1]).toContain(
expect(servicesManager.registerService.mock.calls[1][1]).toEqual(
fakeConfiguration
);
});
@@ -85,7 +85,7 @@ describe('ServicesManager.js', () => {
expect(log.warn.mock.calls.length).toBe(1);
});
it('pass configuration to service create factory function', () => {
it('pass dependencies and configuration to service create factory function', () => {
const configuration = { config: 'Some configuration' };
servicesManager.registerService(fakeService, configuration);
@@ -40,7 +40,9 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) { }
} catch (e) {
throw e;
}
}
if (loaders.next().done && !result) {
@@ -51,8 +53,8 @@ export default class RetrieveMetadataLoader {
}
// Methods to be overwrite
async configLoad() { }
async preLoad() { }
async load(preLoadData) { }
async posLoad(loadData) { }
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
}
-413
View File
@@ -1,413 +0,0 @@
import DICOMWeb from '../DICOMWeb/';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
* @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']),
};
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
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;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration parameters
* @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);
})
);
}
const createStudyFromSOPInstanceList = async (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;
}
throw new Error('Failed to create study out of provided SOP instance list');
};
export { createStudyFromSOPInstanceList, addInstancesToStudy };
+27
View File
@@ -3,6 +3,33 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.6...@ohif/ui@1.1.7) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.5...@ohif/ui@1.1.6) (2019-12-20)
**Note:** Version bump only for package @ohif/ui
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.4...@ohif/ui@1.1.5) (2019-12-19)
### Bug Fixes
* 🐛 Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307)
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.3...@ohif/ui@1.1.4) (2019-12-16)
**Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "1.1.4",
"version": "1.1.7",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -79,7 +79,11 @@ class MeasurementTable extends Component {
</ScrollableArea>
<div className="measurementTableFooter">
{saveFunction && (
<button onClick={this.saveFunction} className="saveBtn">
<button
onClick={this.saveFunction}
className="saveBtn"
data-cy="save-measurements-btn"
>
<Icon name="save" width="14px" height="14px" />
Save measurements
</button>
@@ -43,6 +43,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
? tabs.map((tab, index) => {
const { name, hidden = false } = tab;
const cypressSelectorId = name.toLowerCase();
const tabClass =
index === activeTabIndex ? 'nav-link active' : 'nav-link';
return !hidden ? (
@@ -52,6 +53,7 @@ const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
onHeaderChanged(index);
}}
className={tabClass}
data-cy={cypressSelectorId}
>
<button>{name}</button>
</li>
@@ -152,6 +152,7 @@ const DialogProvider = ({ children, service }) => {
onStart,
onStop,
onDrag,
showOverlay,
} = dialog;
let position =
@@ -160,7 +161,7 @@ const DialogProvider = ({ children, service }) => {
position = centerPositions.find(position => position.id === id);
}
return (
const dragableItem = () => (
<Draggable
key={id}
disabled={!isDraggable}
@@ -217,6 +218,16 @@ const DialogProvider = ({ children, service }) => {
</div>
</Draggable>
);
return (
showOverlay ? (
<div className="Overlay" key={id}>
{dragableItem()}
</div>
) : (
dragableItem()
)
);
});
/**
@@ -238,13 +249,11 @@ const DialogProvider = ({ children, service }) => {
return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
<div className="DraggableArea">
{dialogs.some(dialog => dialog.showOverlay) ? (
<div className="Overlay active">{renderDialogs()}</div>
) : (
renderDialogs()
)}
</div>
{!isEmpty() &&
<div className="DraggableArea">
{renderDialogs()}
</div>
}
{children}
</DialogContext.Provider>
);
@@ -6,17 +6,16 @@
div
cursor: grabbing !important
.DraggableArea, .Overlay
.DraggableArea
width: 100%
height: 100%
position: absolute
.Overlay.active
position: fixed
z-index: 999
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
background: rgba(0,0,0,.1)
.Overlay
position: fixed
z-index: 999
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
background: rgba(0,0,0,.1)
+7 -1
View File
@@ -23,8 +23,14 @@ export function ToolbarButton(props) {
}
};
const cypressSelectorId = props.label.toLowerCase();
return (
<div className={className} onClick={handleClick}>
<div
className={className}
onClick={handleClick}
data-cy={cypressSelectorId}
>
{iconProps && <Icon {...iconProps} />}
<div className="toolbar-button-label">
{t(label)}
+95
View File
@@ -3,6 +3,101 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.3.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.4...@ohif/viewer@3.3.5) (2020-01-06)
**Note:** Version bump only for package @ohif/viewer
## [3.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.3...@ohif/viewer@3.3.4) (2019-12-30)
**Note:** Version bump only for package @ohif/viewer
## [3.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.2...@ohif/viewer@3.3.3) (2019-12-20)
### Bug Fixes
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
## [3.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.1...@ohif/viewer@3.3.2) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
## [3.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.3.0...@ohif/viewer@3.3.1) (2019-12-20)
**Note:** Version bump only for package @ohif/viewer
# [3.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.2...@ohif/viewer@3.3.0) (2019-12-20)
### Features
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
## [3.2.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.1...@ohif/viewer@3.2.2) (2019-12-19)
### Bug Fixes
* 🐛 Fix drag-n-drop of local files into OHIF ([#1319](https://github.com/OHIF/Viewers/issues/1319)) ([23305ce](https://github.com/OHIF/Viewers/commit/23305cec9c0f514e73a8dd17f984ffc87ad8d131)), closes [#1307](https://github.com/OHIF/Viewers/issues/1307)
## [3.2.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.2.0...@ohif/viewer@3.2.1) (2019-12-18)
**Note:** Version bump only for package @ohif/viewer
# [3.2.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.12...@ohif/viewer@3.2.0) (2019-12-16)
### Features
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
## [3.1.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.11...@ohif/viewer@3.1.12) (2019-12-16)
### Bug Fixes
* 🐛 Dismiss all dialogs if leaving viewer route [#1242](https://github.com/OHIF/Viewers/issues/1242) ([#1301](https://github.com/OHIF/Viewers/issues/1301)) ([5c3d8b3](https://github.com/OHIF/Viewers/commit/5c3d8b37b6f723fbd8edcc447c37984e7eee8d40))
## [3.1.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.1.10...@ohif/viewer@3.1.11) (2019-12-16)
**Note:** Version bump only for package @ohif/viewer
@@ -266,7 +266,7 @@ describe('OHIF Cornerstone Toolbar', () => {
let iconName;
//Click on one of the secondary tools from the overlay
cy.get('.tooltip-inner > :nth-child(1)')
cy.get('[data-cy="magnify"]')
.click()
.then($magnifyBtn => {
cy.wrap($magnifyBtn)
@@ -407,9 +407,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.as('toolbarOverlay')
.should('be.visible');
//Click on Clear button
cy.get('.tooltip-inner')
.contains('Clear')
.click();
cy.get('[data-cy="clear"]').click();
//Verify if measurements were removed from the measurements panel
cy.get('.measurementItem').should('not.exist');
@@ -446,9 +444,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.visible')
.then(() => {
//Click Eraser button
cy.get('.tooltip-inner')
.contains('Eraser')
.click({ force: true });
cy.get('[data-cy="eraser"]').click({ force: true });
});
//Erase measurement #1 and Verify if it was removed from the measurements panel
@@ -471,9 +467,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible');
// Click on Invert button
cy.get('.tooltip-inner')
.contains('Invert')
.click();
cy.get('[data-cy="invert"]').click();
// Visual comparison
cy.screenshot('Invert tool - Should Invert Canvas');
@@ -488,9 +482,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.visible')
.then(() => {
//Click on Rotate button
cy.get('.tooltip-inner')
.contains('Rotate')
.click({ force: true });
cy.get('[data-cy="rotate right"]').click({ force: true });
});
// Visual comparison
@@ -505,9 +497,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip H button
cy.get('.tooltip-inner')
.contains('Flip H')
.click();
cy.get('[data-cy="flip h"]').click();
// Visual comparison
cy.screenshot('Flip H tool - Should Flip Image on Y axis');
@@ -521,9 +511,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button
cy.get('.tooltip-inner')
.contains('Flip V')
.click();
cy.get('[data-cy="flip v"]').click();
// Visual comparison
cy.screenshot('Flip V tool - Should Flip Image on X axis');
@@ -1,14 +1,14 @@
//We are keeping the hardcoded results values for the study list tests
//this is intended to be running in a controled docker environment with test data.
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
beforeEach(function () {
describe('OHIF Study List', function() {
context('Desktop resolution', function() {
beforeEach(function() {
cy.viewport(1750, 720);
cy.openStudyList();
cy.initStudyListAliasesOnDesktop();
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@patientName').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -18,7 +18,7 @@ describe('OHIF Study List', function () {
});
});
it('searches MRN with exact string', function () {
it('searches MRN with exact string', function() {
cy.get('@MRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -28,7 +28,7 @@ describe('OHIF Study List', function () {
});
});
it('searches Accession with exact string', function () {
it('searches Accession with exact string', function() {
cy.get('@accessionNumber').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -38,17 +38,17 @@ describe('OHIF Study List', function () {
});
});
it('searches Modality with camel case', function () {
it('searches Modality with camel case', function() {
cy.get('@modalities').type('Mr');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18);
expect($list.length).to.be.eq(17);
expect($list).to.contain('MR');
});
});
it('searches Description with exact string', function () {
it('searches Description with exact string', function() {
cy.get('@studyDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -58,7 +58,7 @@ describe('OHIF Study List', function () {
});
});
it('changes rows per page and checks the study count', function () {
it('changes rows per page and checks the study count', function() {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function () {
// });
});
context('Tablet resolution', function () {
beforeEach(function () {
context('Tablet resolution', function() {
beforeEach(function() {
cy.viewport(1000, 660);
cy.openStudyList();
cy.initStudyListAliasesOnTablet();
});
it('searches Patient Name with exact string', function () {
it('searches Patient Name with exact string', function() {
cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -117,7 +117,7 @@ describe('OHIF Study List', function () {
});
});
it('searches MRN with with exact string', function () {
it('searches MRN with with exact string', function() {
cy.get('@patientNameOrMRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -127,17 +127,17 @@ describe('OHIF Study List', function () {
});
});
it('searches Modality with exact string', function () {
it('searches Modality with exact string', function() {
cy.get('@accessionModalityDescription').type('MR');
//Wait result list to be displayed
cy.waitStudyList();
cy.get('@searchResult').should($list => {
expect($list.length).to.be.eq(18);
expect($list.length).to.be.eq(17);
expect($list).to.contain('MR');
});
});
it('searches Accession with exact string', function () {
it('searches Accession with exact string', function() {
cy.get('@accessionModalityDescription').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -147,7 +147,7 @@ describe('OHIF Study List', function () {
});
});
it('searches Description with exact string', function () {
it('searches Description with exact string', function() {
cy.get('@accessionModalityDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -157,7 +157,7 @@ describe('OHIF Study List', function () {
});
});
it('changes rows per page and checks the study count', function () {
it('changes rows per page and checks the study count', function() {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -24,10 +24,9 @@ describe('OHIF VTK Extension', () => {
cy.get('[data-cy="thumbnail-list"]')
.contains('CT WB 5.0 B35f')
.drag('.viewport-drop-target')
.wait(1000); // TODO: Once issue #1167 is fixed, the wait command should be removed. (https://github.com/OHIF/Viewers/issues/1167)
//Select 2D MPR button
cy.get('.PluginSwitch > .toolbar-button').click();
cy.get('[data-cy="2d mpr"]').click();
//Wait Reformatting Images
cy.waitVTKReformatting();
@@ -0,0 +1,149 @@
describe('OHIF Save Measurements', function() {
before(() => {
cy.openStudy('Fall 1');
cy.expectMinimumThumbnails(2);
});
beforeEach(() => {
// Drags Study thumbnail into viewport
cy.get('[data-cy="thumbnail-list"]:nth-child(1)')
.scrollIntoView()
.drag('.viewport-drop-target');
// Wait image to load on viewport
cy.wait(2000);
cy.resetViewport();
cy.initCommonElementsAliases();
});
it('saves new measurement annotation', function() {
// Add measurement in the viewport
cy.addLengthMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.at.least', 1);
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that success message overlay is displayed
cy.get('.sb-success')
.should('be.visible')
.and('contains.text', 'Measurements were saved with success');
// Visual test comparison
cy.screenshot('Save Measurements - new measurement added');
cy.percyCanvasSnapshot('Save Measurements - new measurement added');
});
it('retrieves saved measurements', function() {
// Add measurement in the viewport
cy.addLengthMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementDisplayText') // Get label size of the recently added measurement
.last()
.then($measurementSizeLabel => {
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that success message overlay is displayed
cy.get('.sb-success').should('be.visible');
// Reload the page
cy.reload();
//Verify that recently added measurement was retrieved
cy.get('@measurementsBtn').click();
cy.get('.measurementDisplayText') // Get label size of the recently added measurement
.last()
.then($retrivedMeasurementSizeLabel => {
expect($retrivedMeasurementSizeLabel.textContent).to.eq(
$measurementSizeLabel.textContent
);
});
});
});
it('checks error message when saving without any measurement', function() {
// Checks that measurement list is empty
cy.get('.numberOfItems').should('have.text', '0');
// Click on Save Measurement button
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that error message overlay is displayed
cy.get('.sb-error')
.should('be.visible')
.and('contains.text', 'Error while saving the measurements');
// Close message overlay
cy.get('.sb-closeIcon').click();
});
it('checks if warning message is displayed on measurements of unsupported tools', function() {
// Add measurement for unsupported tool in the viewport
cy.addAngleMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.at.least', 1);
// Check that warning is displayed for unsupported tool
cy.get('.hasWarnings').should('be.visible');
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that error message overlay is displayed
cy.get('.sb-error')
.should('be.visible')
.and('contains.text', 'Error while saving the measurements');
// Close message overlay
cy.get('.sb-closeIcon').click();
// Close Measurements panel
cy.get('@measurementsBtn').click();
});
it('checks if measurements of unsupported tools were not saved', function() {
// Add measurement for supported tool in the viewport
cy.addLengthMeasurement();
// Add measurement for unsupported tool in the viewport
cy.addAngleMeasurement();
// Verify if measurement annotation was added into the measurements panel
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 2);
// Check that warning is displayed for unsupported tool
cy.get('.hasWarnings').should('be.visible');
// Save new measurement
cy.get('[data-cy="save-measurements-btn"]').click();
// Verify that success message overlay is displayed
cy.get('.sb-success')
.should('be.visible')
.and('contains.text', 'Measurements were saved with success');
// Reload the page
cy.reload();
//Verify that measurement for unsupported tool was not saved
cy.get('@measurementsBtn').click();
cy.get('.measurementItem')
.its('length')
.should('be.eq', 1);
// Close Measurements panel
cy.get('@measurementsBtn').click();
});
});
+18 -22
View File
@@ -1,16 +1,16 @@
//Creating aliases for Cornerstone tools buttons
export function initCornerstoneToolsAliases() {
cy.get('.ToolbarRow > :nth-child(2)').as('stackScrollBtn');
cy.get('.ToolbarRow > :nth-child(3)').as('zoomBtn');
cy.get('.ToolbarRow > :nth-child(4)').as('levelsBtn');
cy.get('.ToolbarRow > :nth-child(5)').as('panBtn');
cy.get('.ToolbarRow > :nth-child(6)').as('lengthBtn');
cy.get('.ToolbarRow > :nth-child(7)').as('annotateBtn');
cy.get('.ToolbarRow > :nth-child(8)').as('angleBtn');
cy.get('.ToolbarRow > :nth-child(9)').as('resetBtn');
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
cy.get('.expandableToolMenu').as('moreBtn');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('[data-cy="stack scroll"]').as('stackScrollBtn');
cy.get('[data-cy="zoom"]').as('zoomBtn');
cy.get('[data-cy="levels"]').as('levelsBtn');
cy.get('[data-cy="pan"]').as('panBtn');
cy.get('[data-cy="length"]').as('lengthBtn');
cy.get('[data-cy="annotate"]').as('annotateBtn');
cy.get('[data-cy="angle"]').as('angleBtn');
cy.get('[data-cy="reset"]').as('resetBtn');
cy.get('[data-cy="cine"]').as('cineBtn');
cy.get('[data-cy="more"]').as('moreBtn');
cy.get('[data-cy="layout"]').as('layoutBtn');
cy.get('.viewport-element').as('viewport');
}
@@ -44,14 +44,14 @@ export function initRouteAliases() {
//Creating aliases for VTK tools buttons
export function initVTKToolsAliases() {
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
cy.get('.ToolbarRow > :nth-child(2)').as('crosshairsBtn');
cy.get('.ToolbarRow > :nth-child(3)').as('wwwcBtn');
cy.get('.ToolbarRow > :nth-child(4)').as('rotateBtn');
cy.get('[data-cy="exit 2d mpr"]').as('exit2dmprBtn');
cy.get('[data-cy="crosshairs"]').as('crosshairsBtn');
cy.get('[data-cy="wwwc"]').as('wwwcBtn');
cy.get('[data-cy="rotate"]').as('rotateBtn');
cy.get('.slab-thickness').as('slabSlider');
cy.get('.select-ohif').as('modeDropdown');
cy.get('.ohif-check-label').as('modeCheckbox');
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
cy.get('[data-cy="layout"]').as('layoutBtn');
}
//Creating aliases for Study List page elements on Desktop experience
@@ -82,12 +82,8 @@ export function initStudyListAliasesOnTablet() {
//Creating aliases for User Preferences modal
export function initPreferencesModalAliases() {
cy.get('.OHIFModal').as('preferencesModal');
cy.get('.nav-link')
.first()
.as('userPreferencesHotkeysTab');
cy.get('.nav-link')
.last()
.as('userPreferencesGeneralTab');
cy.get('[data-cy="hotkeys"]').as('userPreferencesHotkeysTab');
cy.get('[data-cy="general"]').as('userPreferencesGeneralTab');
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
cy.get('[data-cy="save-btn"]').as('saveBtn');
+10 -12
View File
@@ -209,7 +209,7 @@ Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
//Command to reset and clear all the changes made to the viewport
Cypress.Commands.add('resetViewport', () => {
//Click on More button
cy.get('.expandableToolMenu')
cy.get('[data-cy="more"]')
.as('moreBtn')
.click();
//Verify if overlay is displayed
@@ -219,11 +219,11 @@ Cypress.Commands.add('resetViewport', () => {
}
});
//Click on Clear button
cy.get('.tooltip-inner > :nth-child(10)')
cy.get('[data-cy="clear"]')
.as('clearBtn')
.click();
//Click on Reset button
cy.get('.ToolbarRow > :nth-child(9)')
cy.get('[data-cy="reset"]')
.as('resetBtn')
.click();
});
@@ -284,9 +284,8 @@ Cypress.Commands.add('initStudyListAliasesOnTablet', () => {
Cypress.Commands.add(
'addLengthMeasurement',
(firstClick = [150, 100], secondClick = [130, 170]) => {
cy.initCornerstoneToolsAliases();
cy.get('@lengthBtn').click();
cy.addLine('@viewport', firstClick, secondClick);
cy.get('[data-cy="length"]').click();
cy.addLine('.viewport-element', firstClick, secondClick);
}
);
@@ -294,9 +293,8 @@ Cypress.Commands.add(
Cypress.Commands.add(
'addAngleMeasurement',
(initPos = [180, 390], midPos = [300, 410], finalPos = [180, 450]) => {
cy.initCornerstoneToolsAliases();
cy.get('@angleBtn').click();
cy.addAngle('@viewport', initPos, midPos, finalPos);
cy.get('[data-cy="angle"]').click();
cy.addAngle('.viewport-element', initPos, midPos, finalPos);
}
);
@@ -369,7 +367,7 @@ Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
});
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
cy.get('.btn-group > .toolbar-button').click();
cy.get('[data-cy="layout"]').click();
cy.get('.layoutChooser')
.find('tr')
@@ -471,12 +469,12 @@ Cypress.Commands.add(
Cypress.Commands.add('openDownloadImageModal', () => {
// Click on More button
cy.get('.expandableToolMenu')
cy.get('[data-cy="more"]')
.as('moreBtn')
.click();
// Click on Download button
cy.get('.tooltip-inner > :nth-child(13)')
cy.get('[data-cy="download"]')
.as('downloadBtn')
.click();
});
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "3.1.11",
"version": "3.3.5",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -45,20 +45,20 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^2.1.1",
"@ohif/extension-cornerstone": "^2.1.1",
"@ohif/core": "^2.3.2",
"@ohif/extension-cornerstone": "^2.2.1",
"@ohif/extension-dicom-html": "^1.1.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.1",
"@ohif/extension-vtk": "^1.1.4",
"@ohif/extension-vtk": "^1.2.5",
"@ohif/i18n": "^0.52.4",
"@ohif/ui": "^1.1.4",
"@ohif/ui": "^1.1.7",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
"cornerstone-core": "^2.2.8",
"cornerstone-math": "^0.1.8",
"cornerstone-tools": "^4.8.0",
"cornerstone-tools": "^4.9.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.8.2",
"dicom-parser": "^1.8.3",
+10 -6
View File
@@ -64,10 +64,7 @@ const commandsManagerConfig = {
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
let extensionManager;
/** ~~~~~~~~~~~~~ End Application Setup */
// TODO[react] Use a provider when the whole tree is React
@@ -128,7 +125,8 @@ class App extends Component {
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(
[...defaultExtensions, ...extensions],
cornerstoneExtensionConfig
cornerstoneExtensionConfig,
this._appConfig
);
/*
@@ -241,7 +239,13 @@ function _initServices(services) {
/**
* @param
*/
function _initExtensions(extensions, cornerstoneExtensionConfig) {
function _initExtensions(extensions, cornerstoneExtensionConfig, appConfig) {
extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig,
});
const requiredExtensions = [
GenericViewerCommands,
[OHIFCornerstoneExtension, cornerstoneExtensionConfig],
@@ -1,9 +1,8 @@
import { connect } from 'react-redux';
import { MeasurementTable } from '@ohif/ui';
import OHIF from '@ohif/core';
import OHIF, { DICOMSR } from '@ohif/core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
import DICOMSR from './../../lib/DICOMSR';
import jumpToRowItem from './jumpToRowItem.js';
@@ -32,9 +31,7 @@ function groupBy(list, props) {
*/
function getAllTools(toolGroups = []) {
let tools = [];
toolGroups.forEach(
toolGroup => (tools = tools.concat(toolGroup.childTools))
);
toolGroups.forEach(toolGroup => (tools = tools.concat(toolGroup.childTools)));
return tools;
}
@@ -80,6 +80,7 @@ export default function init({
id: 'labelling',
isDraggable: false,
showOverlay: true,
centralize: true,
content: LabellingFlow,
contentProps: {
measurementData,
@@ -1,72 +0,0 @@
import OHIF from '@ohif/core';
import React from 'react';
import PluginSwitch from './PluginSwitch.js';
import { commandsManager } from './../App.js';
import { connect } from 'react-redux';
const { setLayout } = OHIF.redux.actions;
const ConnectedPluginSwitch = props => {
return <PluginSwitch {...props} />;
};
const mapStateToProps = state => {
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
return {
activeViewportIndex,
viewportSpecificData,
layout,
};
};
const mapDispatchToProps = dispatch => {
return {
setLayout: data => {
dispatch(setLayout(data));
},
};
};
/*function setSingleLayoutData(originalArray, viewportIndex, data) {
const viewports = originalArray.slice();
const layoutData = Object.assign({}, viewports[viewportIndex], data);
viewports[viewportIndex] = layoutData;
return viewports;
}*/
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
const { activeViewportIndex, viewportSpecificData } = propsFromState;
const { studies } = ownProps;
const { setLayout } = propsFromDispatch;
const mpr = () => {
commandsManager.runCommand('mpr2d');
};
const exitMpr = () => {
const layout = {
numRows: 1,
numColumns: 1,
viewports: [{ plugin: 'cornerstone' }],
};
setLayout(layout);
};
return {
mpr,
exitMpr,
activeViewportIndex,
viewportSpecificData,
studies,
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(ConnectedPluginSwitch);
@@ -1,3 +0,0 @@
.icon-rotate-120 {
transform: rotate(120deg);
}
@@ -1,106 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ToolbarButton } from '@ohif/ui';
import './PluginSwitch.css';
class PluginSwitch extends Component {
static propTypes = {
mpr: PropTypes.func,
activeViewportIndex: PropTypes.number,
viewportSpecificData: PropTypes.object,
studies: PropTypes.array,
exitMpr: PropTypes.func,
};
static defaultProps = {};
constructor(props) {
super(props);
this.state = {
isPlugSwitchOn: false,
label: '2D MPR',
icon: 'cube',
};
}
handleClick = () => {
if (this.state.isPlugSwitchOn) {
this.setState({
isPlugSwitchOn: false,
label: '2D MPR',
icon: 'cube',
});
this.props.exitMpr();
} else {
this.setState({
isPlugSwitchOn: true,
label: 'Exit 2D MPR',
icon: 'times',
});
this.props.mpr();
}
};
render() {
const { label, icon } = this.state;
// Render exit mpr if switched on, otherwise check if mpr button should be displayed.
const shouldRender =
this.state.isPlugSwitchOn || _shouldRenderMpr2DButton.call(this);
return (
<>
{shouldRender && (
<div className="PluginSwitch">
<ToolbarButton
label={label}
icon={icon}
onClick={this.handleClick}
/>
</div>
)}
</>
);
}
}
function _shouldRenderMpr2DButton() {
const { viewportSpecificData, studies, activeViewportIndex } = this.props;
if (!viewportSpecificData[activeViewportIndex]) {
return;
}
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
activeViewportIndex
];
const displaySet = _getDisplaySet(
studies,
studyInstanceUid,
displaySetInstanceUid
);
if (!displaySet) {
return;
}
return displaySet.isReconstructable;
}
function _getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
const study = studies.find(
study => study.studyInstanceUid === studyInstanceUid
);
if (!study) {
return;
}
const displaySet = study.displaySets.find(set => {
return set.displaySetInstanceUid === displaySetInstanceUid;
});
return displaySet;
}
export default PluginSwitch;
@@ -16,7 +16,6 @@ import { commandsManager, extensionManager } from './../App.js';
import ConnectedCineDialog from './ConnectedCineDialog';
import ConnectedLayoutButton from './ConnectedLayoutButton';
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
class ToolbarRow extends Component {
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
@@ -124,7 +123,6 @@ class ToolbarRow extends Component {
</div>
{buttonComponents}
<ConnectedLayoutButton />
<ConnectedPluginSwitch studies={this.props.studies} />
<div
className="pull-right m-t-1 rm-x-1"
style={{ marginLeft: 'auto' }}
@@ -3,7 +3,8 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MODULE_TYPES } from '@ohif/core';
import OHIF from '@ohif/core';
import OHIF, { DICOMSR } from '@ohif/core';
import { withDialog } from '@ohif/ui';
import moment from 'moment';
import ConnectedHeader from './ConnectedHeader.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
@@ -11,7 +12,6 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
import { extensionManager } from './../App.js';
import DICOMSR from '../lib/DICOMSR';
// Contexts
import WhiteLabellingContext from '../context/WhiteLabellingContext.js';
@@ -69,6 +69,7 @@ class Viewer extends Component {
// window.store.getState().viewports.activeViewportIndex
activeViewportIndex: PropTypes.number.isRequired,
isStudyLoaded: PropTypes.bool,
dialog: PropTypes.object,
};
constructor(props) {
@@ -104,6 +105,12 @@ class Viewer extends Component {
thumbnails: [],
};
componentWillUnmount() {
if (this.props.dialog) {
this.props.dialog.dismissAll();
}
}
retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints');
@@ -297,11 +304,11 @@ class Viewer extends Component {
activeIndex={this.props.activeViewportIndex}
/>
) : (
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
</SidePanel>
{/* MAIN */}
@@ -324,7 +331,7 @@ class Viewer extends Component {
}
}
export default Viewer;
export default withDialog(Viewer);
/**
* What types are these? Why do we have "mapping" dropped in here instead of in
@@ -337,7 +344,7 @@ export default Viewer;
* @param {Study[]} studies
* @param {DisplaySet[]} studies[].displaySets
*/
const _mapStudiesToThumbnails = function (studies) {
const _mapStudiesToThumbnails = function(studies) {
return studies.map(study => {
const { studyInstanceUid } = study;
+14 -4
View File
@@ -5962,10 +5962,10 @@ cornerstone-math@^0.1.8:
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5"
integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw==
cornerstone-tools@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.8.0.tgz#1972546e13e9a09b8aa25a3541ffc345283eec01"
integrity sha512-PO7/jYbVwc+ddF9JW/o/JXqNPFZV7y1PUTzlRATC9FeXLXNKIkSFBCF5SA1EGJoyjzRYGDBgTZuTYhfm96AlAA==
cornerstone-tools@^4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.9.0.tgz#8b68603c32aceb84baf860f4dd0d36ac176e8b75"
integrity sha512-6yHzXm4qn7VuHMRiaV/oS3khdMidtTho6P1OIYICqZ2pmorsH8jN8LvNgqz+Ky9Nf+wvrvi7ul8ZzcD1sJTkWA==
dependencies:
"@babel/runtime" "7.1.2"
cornerstone-math "0.1.7"
@@ -6545,6 +6545,16 @@ dcmjs@^0.8.2:
loglevelnext "^3.0.1"
ndarray "^1.0.18"
dcmjs@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.8.3.tgz#fff1b030b6cb2d6e2afb1aa99840bfa853724c31"
integrity sha512-eXQjqgtJf9+oseraKDNDm2A5F3Th4B2GJeZtjStj0IFXxjlbPOzdq3PfCyxdwfRaKOBIwr0q3YK/Vfs2CpQY8Q==
dependencies:
"@babel/polyfill" "^7.6.0"
"@babel/runtime" "^7.6.3"
loglevelnext "^3.0.1"
ndarray "^1.0.18"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.1, debug@^2.6.3, debug@^2.6.6, debug@^2.6.8, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"