Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1a4c09b74 | ||
|
|
02572b4928 | ||
|
|
fd1d29820a | ||
|
|
6c63a917d3 | ||
|
|
caa6062c41 | ||
|
|
7f9743a020 | ||
|
|
a2c4161477 | ||
|
|
42cfce60f5 | ||
|
|
244949bab6 | ||
|
|
e87ec7b7f5 | ||
|
|
d98889f1eb |
No files matched your search
@@ -1,23 +1,10 @@
|
||||
### Request Checklist
|
||||
### PR Checklist
|
||||
|
||||
- [] Brief description of changes
|
||||
- [] Links to any relevant issues
|
||||
- [] Required status checks are passing
|
||||
- [] `@mention` a maintainer to request a review
|
||||
|
||||
#### Special
|
||||
|
||||
> If this PR is not on a branch for this repo, it is "untrusted" and does not
|
||||
> have access to env vars. Reviewers can kick off a trusted docker deploy by:
|
||||
>
|
||||
> 1. Verifying there are no malicious changes to CI build
|
||||
> 2. Using the [git-push-fork-to-upstream-branch][script] script
|
||||
> 3. Executing the manual approval step in the CI workflow
|
||||
|
||||
- [] Request docker publish of PR
|
||||
|
||||
_Want to improve this process? Consider PR'ing the ability for reviewers to add
|
||||
a label to trigger this process._
|
||||
- [ ] Brief description of changes
|
||||
- [ ] Links to any relevant issues
|
||||
- [ ] Required status checks are passing
|
||||
- [ ] User cases if changes impact the user's experience
|
||||
- [ ] `@mention` a maintainer to request a review
|
||||
|
||||
<!--
|
||||
Links
|
||||
|
||||
@@ -21,11 +21,24 @@
|
||||
- [Extensions](advanced/architecture.md#misc-extensions)
|
||||
- [Diagram](advanced/architecture.md#diagram)
|
||||
- [Common Questions](advanced/architecture.md#common-questions)
|
||||
- [Extensions](advanced/extensions.md)
|
||||
- [Overview](advanced/extensions.md#overview)
|
||||
- [Modules](advanced/extensions.md#modules)
|
||||
- [Registering](advanced/extensions.md#registering-extensions)
|
||||
- [OHIF Maintained](advanced/extensions.md#ohif-maintained-extensions)
|
||||
- [Extensions](extensions/index.md)
|
||||
- [Registering](extensions/index.md#registering-an-extension)
|
||||
- [Lifecycle Hooks](extensions/index.md#lifecycle-hooks)
|
||||
- [preRegistration](extensions/lifecycle/pre-registration.md)
|
||||
- [Modules](extensions/index.md#modules)
|
||||
- [Commands](extensions/modules/commands.md)
|
||||
- [Panel](extensions/modules/panel.md)
|
||||
- [SOP Class Handler](extensions/modules/sop-class-handler.md)
|
||||
- [Toolbar](extensions/modules/toolbar.md)
|
||||
- [Viewport](extensions/modules/viewport.md)
|
||||
- [Contexts](extension/index.md#contexts)
|
||||
- [OHIF Maintained](extension/index.md#maintained-extensions)
|
||||
- [Services](services/index.md)
|
||||
- [Default](services/default/index.md)
|
||||
- [UI](services/ui/index.md)
|
||||
- [Dialog Service](services/ui/ui-dialog-service.md)
|
||||
- [Modal Service](services/ui/ui-modal-service.md)
|
||||
- [Notification Service](services/ui/ui-notification-service.md)
|
||||
- [Custom Tools](advanced/custom-tools.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -94,16 +94,16 @@ the viewer. Some common use cases include:
|
||||
- Adding surgical templating tools and reports
|
||||
- and many others
|
||||
|
||||
We expose common integration points via [extensions](./extensions.md) to make
|
||||
this possible. The viewer and many of our own extensions also offer
|
||||
We expose common integration points via [extensions](../extensions/index.md) to
|
||||
make this possible. The viewer and many of our own extensions also offer
|
||||
[configuration][configuration]. For a list of extensions maintained by OHIF,
|
||||
[check out this helpful table](./extensions.html#ohif-maintained-extensions).
|
||||
[check out this helpful table](../extensions/index.mdl#maintained-extensions).
|
||||
|
||||
If you find yourself thinking "I wish the Viewer could do X", and you can't
|
||||
accomplish it with an extension today, create a GitHub issue! We're actively
|
||||
looking for ways to improve our extensibility ^\_^
|
||||
|
||||
[Click here to read more about extensions!](./extensions.md)
|
||||
[Click here to read more about extensions!](../extensions/index.md)
|
||||
|
||||
### Diagram
|
||||
|
||||
@@ -145,7 +145,7 @@ you would need to recreate.
|
||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
||||
[pwa]: https://developers.google.com/web/progressive-web-apps/
|
||||
[configuration]: ../essentials/configuration.md
|
||||
[extensions]: ./extensions.md
|
||||
[extensions]: ../extensions/index.md
|
||||
[core-github]: https://github.com/OHIF/viewers/platform/core
|
||||
[ui-github]: https://github.com/OHIF/Viewers/platform/ui
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
This is not yet exposed in an easy/convenient way. Most tools are currently
|
||||
added by creating new Viewport, Toolbar, and SOPInstanceHandler extension
|
||||
modules. You can read more about that approach in [extensions](./extensions.md).
|
||||
modules. You can read more about that approach in
|
||||
[extensions](../extensions/index.md).
|
||||
|
||||
In the near future, we intend to improve the extensibility of tools for existing
|
||||
Viewports (like our Cornerstone.js and VTK.js viewports).
|
||||
@@ -1,263 +0,0 @@
|
||||
# Extensions
|
||||
|
||||
Extensions add new functionality to the viewer by registering one or more
|
||||
modules. They go one step further than configuration in that they allow us to
|
||||
inject custom React components, so long as they adhere to the module's
|
||||
interface. This can be something as simple as adding a new button to the
|
||||
toolbar, or as complex as a new viewport capable of rendering volumes in 3D.
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Modules](#modules)
|
||||
- [Commands](#commands)
|
||||
- [Hotkeys](#hotkeys)
|
||||
- [Toolbar](#toolbar)
|
||||
- [Panel](#panel)
|
||||
- [Viewport](#viewport)
|
||||
- [SOP Class Handler](#sopclasshandler)
|
||||
|
||||
## Overview
|
||||
|
||||
At a glance, an extension is a javascript object that has an `id` property, and
|
||||
one or more "module" methods. You can find an abbreviated extension below, or
|
||||
[view the source][example-ext-src] of our example extension.
|
||||
|
||||
```js
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'example-extension',
|
||||
|
||||
/**
|
||||
* Registers one or more named commands scoped to a context. Commands are
|
||||
* the primary means for...
|
||||
*/
|
||||
getCommandsModule() {
|
||||
return {
|
||||
defaultContext: 'VIEWER'
|
||||
actions: { ... },
|
||||
definitions: { ... }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Allows you to provide toolbar definitions that will be merged with any
|
||||
* existing application toolbar configuration. Used to determine which
|
||||
* buttons should be visible when, their order, what happens when they're
|
||||
* clicked, etc.
|
||||
*/
|
||||
getToolbarModule() {
|
||||
return {
|
||||
definitions: [ ... ],
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented
|
||||
*/
|
||||
getPanelModule: () => null,
|
||||
|
||||
/**
|
||||
* Registers a ReactComponent that should be used to render data in a
|
||||
* Viewport. The first registered viewport is our "default viewport". If
|
||||
* more than one viewport is registered, we use `SopClassHandlers` to
|
||||
* determine which viewport should be used.
|
||||
*/
|
||||
getViewportModule: () => reactViewportComponent,
|
||||
|
||||
/** Provides a whitelist of SOPClassUIDs the viewport is capable of rendering.
|
||||
* Can modify default behavior for methods like `getDisplaySetFromSeries` */
|
||||
getSopClassHandler: () => {
|
||||
id: 'some-other-unique-id',
|
||||
sopClassUids: [ ... ],
|
||||
getDisplaySetFromSeries: (series, study, dicomWebClient, authorizationHeaders) => { ... }
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Modules
|
||||
|
||||
There are a few different module types. Each module type allows us to extend the
|
||||
viewer in a different way, and provides a consistent API for us to do so. You
|
||||
can find a full list of the different types of modules
|
||||
[`in ohif-core`][module-types]. Information on each type of module, it's API,
|
||||
and how we determine when/where it should be used is included below.
|
||||
|
||||
> NOTE: Modifying the extensions/modules registered to the OHIF Viewer currently
|
||||
> requires us to import and pass extensions to the ExtensionManager in
|
||||
> `src/App.js`, then rebuild the application. Long-term, we intend to make it
|
||||
> possible to accomplish this without a build step.
|
||||
|
||||
#### Commands
|
||||
|
||||
The Commands Module allows us to register one or more commands scoped to
|
||||
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
|
||||
and any registered custom react component (like a [viewport][#] or [panel][#]).
|
||||
Here is a simple example commands module:
|
||||
|
||||
```js
|
||||
{
|
||||
getCommandsModule() {
|
||||
return {
|
||||
actions: {
|
||||
speak: ({ viewports, words }) => {
|
||||
console.log(viewports, words);
|
||||
},
|
||||
},
|
||||
definitions: {
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 }
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
context: 'ACTIVE_VIEWER::CORNERSTONE'
|
||||
},
|
||||
},
|
||||
defaultContext: 'VIEWER'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Viewport
|
||||
|
||||
An extension can register a Viewport Module by providing a `getViewportModule()`
|
||||
method that returns a React Component. The React component will receive the
|
||||
following props:
|
||||
|
||||
```js
|
||||
children: PropTypes.arrayOf(PropTypes.element)
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportData: PropTypes.object, // { studies, displaySet }
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object
|
||||
```
|
||||
|
||||
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||
component is used depends on:
|
||||
|
||||
- The Layout Configuration
|
||||
- Registered SopClassHandlers
|
||||
- The SopClassUID for visible/selected datasets
|
||||
|
||||

|
||||
|
||||
<center><i>An example of three Viewports</i></center>
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
|
||||
|
||||
#### Toolbar
|
||||
|
||||
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
||||
method that returns a React Component. The component does not receive any props.
|
||||
If you want to modify or react to state, you will need to connect to the redux
|
||||
store. The given toolbar must determine its set of elements and the context of
|
||||
them. The set of elements will be listed on toolbar `definitions`.
|
||||
|
||||

|
||||
|
||||
<center><i>A toolbar extension example</i></center>
|
||||
|
||||
Toolbar components are rendered in the `ToolbarRow` component.
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
||||
|
||||
##### Toolbar Custom Component
|
||||
|
||||
Toolbar elements can define its own custom react component to be consumed when
|
||||
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
|
||||
you just need to expose your `CustomToolbarComponent` as the value of key
|
||||
`CustomComponent`. In case the property `CustomComponent` is not present, a
|
||||
default toolbar component will be used to render it. See bellow
|
||||
|
||||
```js
|
||||
definitions: [
|
||||
...
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
`CustomComponent` components will receive the following props:
|
||||
|
||||
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
|
||||
instance)
|
||||
- toolbarClickCallback: callback method when clicking on toolbar
|
||||
- button: its own definition object
|
||||
- key: react key prop
|
||||
- activeButtons: list of active elements
|
||||
- isActive: if current
|
||||
|
||||
#### SopClassHandler
|
||||
|
||||
...
|
||||
|
||||
#### Panel
|
||||
|
||||
> The panel module is not yet in use.
|
||||
|
||||
#### Hotkeys
|
||||
|
||||
...
|
||||
|
||||
### Registering Extensions
|
||||
|
||||
Extensions are registered for the application at startup. The
|
||||
`ExtensionManager`, exposed by `ohif-core`, registers a list of extensions with
|
||||
our application's store. Each module provided by the extension becomes available
|
||||
via `state.plugins.availablePlugins`, and consists of three parts: id, type
|
||||
([PLUGIN_TYPE](https://github.com/OHIF/ohif-core/blob/43c08a29eff3fb646a0e83a03a236ddd84f4a6e8/src/plugins.js#L1-L6)),
|
||||
and the return value of the module method.
|
||||
|
||||
In a future version, we will likely expose a way to provide the extensions you
|
||||
would like included at startup.
|
||||
|
||||
_app.js_
|
||||
|
||||
```js
|
||||
import { createStore, combineReducers } from 'redux';
|
||||
import OHIF from '@ohif/core';
|
||||
import OHIFCornerstoneExtension from 'ohif-cornerstone-extension';
|
||||
|
||||
const combined = combineReducers(OHIF.redux.reducers);
|
||||
const store = createStore(combined);
|
||||
const extensions = [new OHIFCornerstoneExtension()];
|
||||
|
||||
// Dispatches the `addPlugin` action to the store
|
||||
// Adding extension modules to `state.plugins.availablePlugins`
|
||||
ExtensionManager.registerExtensions(store, extensions);
|
||||
```
|
||||
|
||||
## OHIF Maintained Extensions
|
||||
|
||||
A small number of powerful extensions for popular use cases are maintained by
|
||||
OHIF. They're co-located in the
|
||||
[`OHIF/Viewers`](https://github.com/OHIF/Viewers) repository, in the top level
|
||||
[`extensions/`](https://github.com/OHIF/Viewers/tree/master/extensions)
|
||||
directory.
|
||||
|
||||
{% include "./_maintained-extensions-table.md" %}
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[example-ext-src]: https://github.com/OHIF/Viewers/tree/master/extensions/_example/src
|
||||
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
|
||||
<!-- prettier-ignore-end -->
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 260.4 82.9" enable-background="new 0 0 260.4 82.9" xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#525DF9" d="M31.7,8.9c9.6,0,16.5,4.9,19,13.4l0.1,0.4h9.6l-0.1-0.6C57.6,8.5,46.7,0,31.8,0C13.7,0,0,14.1,0,32.9 s13.7,32.9,31.8,32.9c14.9,0,25.8-8.5,28.6-22.1l0.1-0.6h-9.6l-0.1,0.4c-2.5,8.5-9.5,13.4-19,13.4c-13.1,0-21.9-9.6-21.9-24 C9.8,18.6,18.6,8.9,31.7,8.9z"/>
|
||||
<path fill="#525DF9" d="M110.1,58c-2.3,0-3.5-1.4-3.5-4.2V32.7c0-8.8-7.2-14.7-17.9-14.7c-7.5,0-16.7,3.9-18.3,14.9l-0.1,0.6h8.4 l0.1-0.4c1.4-5.7,5.8-6.9,9.3-6.9c6,0,9.5,2.7,9.5,7.4v2.6l-13.6,1.4c-7.6,0.8-15.7,5-15.7,14.6c0,8.1,6,13.5,14.8,13.5 c6.5,0,11.8-3.4,15-6.8c1.3,4.2,4.5,6.5,9.1,6.5c1.7,0,3.2-0.3,5.2-0.9l0.3-0.1v-7l-0.6,0.2C111.5,57.9,110.9,58,110.1,58z M97.7,43.5v7.3c-4.6,4.6-8.9,6.7-13.5,6.7c-2,0-6.8-0.6-6.8-5.7c0-3.8,2.8-6.3,7.4-6.9L97.7,43.5z"/>
|
||||
<path fill="#525DF9" d="M146.5,18c-6.5,0-12,3.1-15.2,6.1v-5.2h-8.9v46h8.9V34.3c2.2-3.2,6.7-8.3,13-8.3c5.6,0,8.7,3.1,8.7,8.8 v30.1h8.9V33.3C161.9,23.7,156.1,18,146.5,18z"/>
|
||||
<path fill="#525DF9" d="M195.8,18c-6.5,0-12,3.1-15.2,6.1v-5.2h-8.9v46h8.9V34.3c2.2-3.2,6.7-8.3,13-8.3c5.6,0,8.7,3.1,8.7,8.8 v30.1h8.9V33.3C211.2,23.7,205.4,18,195.8,18z"/>
|
||||
<polygon fill="#525DF9" points="251.3,18.9 238.6,51.8 237.8,47.9 225.6,18.9 216.2,18.9 234.3,62.4 226,82.9 235,82.9 260.4,18.9 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 445.9 638" enable-background="new 0 0 445.9 638" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#5F5DF9" d="M224.5,638C101.1,638,0,537.8,0,414.7V223.3C0,100.2,101.1,0,224.5,0c109.6,0,202.8,78.3,220.9,186.1
|
||||
c2.9,17.4-8.6,33.8-26,36.8c-17.4,2.9-33.7-8.8-36.7-26.2c-13-77-78.6-132.9-157-132.9c-88.2,0-159.3,71.6-159.3,159.5v191.4
|
||||
c0,88,71.1,159.5,159.3,159.5c78.3,0,144.2-55.9,157.1-132.9c2.9-17.4,19.3-29.1,36.6-26.2c17.4,2.9,28.6,19.4,25.7,36.7
|
||||
C427.2,559.7,334,638,224.5,638z"/>
|
||||
</g>
|
||||
<g opacity="0.5">
|
||||
<path fill="#5F5DF9" d="M153.6,347.7c-17.6,0-30.7-14.3-30.7-31.9v-92.3c0-56.8,45.8-103,102.8-103c38.9,0,73.9,21.6,91.7,56.4
|
||||
c8,15.7,1.7,34.9-14,42.9c-15.7,8-35,1.8-43-13.9c-6.8-13.4-21-21.7-35.8-21.7c-21.8,0-40.2,17.6-40.2,39.2v92.3
|
||||
C184.3,333.4,171.2,347.7,153.6,347.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -35,8 +35,7 @@ benefits, but comes at the cost of time and complexity. Some benefits include:
|
||||
|
||||
_Today:_
|
||||
|
||||
- Leverage [extensions](/advanced/extensions.md) to drop-in powerful new
|
||||
features
|
||||
- Leverage [extensions](/extensions/index.md) to drop-in powerful new features
|
||||
- Add routes and customize the viewer's workflow
|
||||
- Finer control over styling and whitelabeling
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
|
||||
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
|
||||
|
||||
You can read more about extensions and how to create your own in our
|
||||
[extensions guide](/advanced/extensions.md).
|
||||
[extensions guide](/extensions/index.md).
|
||||
|
||||
#### FAQ
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Extension</th>
|
||||
<th>Description</th>
|
||||
<th>Modules</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- CORNERSTONE.js -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-cornerstone">
|
||||
Cornerstone
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>cornerstone.js</code>. Adds support for 2D DICOM rendering and manipulation, as well as support for the tools features in <a href="https://tools.cornerstonejs.org/examples/"><code>cornerstone-tools</code></a>. Also adds "CINE Dialog" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<!-- VTK.js -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-vtk">
|
||||
VTK.js
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>vtk.js</code>. Adds support for volume renderings and advanced features like MPR. Also adds "3D Rotate" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders text and HTML content for <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12">specific SopClassUIDs</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders Microscopy images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,151 @@
|
||||
# Extensions
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Concepts](#concepts)
|
||||
- [Extension Skeleton](#extension-skeleton)
|
||||
- [Registering an Extension](#registering-an-extension)
|
||||
- [Lifecylce Hooks](#lifecycle-hooks)
|
||||
- [Modules](#modules)
|
||||
- [Contexts](#contexts)
|
||||
- [Consuming Extensions](#consuming-extensions)
|
||||
- [Maintained Extensions](#maintained-extensions)
|
||||
|
||||
## Overview
|
||||
|
||||
We use extensions to help us isolate and package groups of related features.
|
||||
Extensions provide functionality, ui components, and new behaviors.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/extensions-diagram.png">
|
||||
<img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Diagram showing how extensions are configured and accessed.</i></div>
|
||||
</div>
|
||||
|
||||
The `@ohif/viewer`'s application level configuration gives us the ability to add
|
||||
and configure extensions. When the application starts, extensions are registered
|
||||
with the `ExtensionManager`. Different portions of the `@ohif/viewer` project
|
||||
will use registered extensions to influence application behavior.
|
||||
|
||||
Extensions allow us to:
|
||||
|
||||
- Wrap and integrate functionality of 3rd party dependencies in a reusable way
|
||||
- Change how application data is mapped and transformed
|
||||
- Display a consistent/cohesive UI
|
||||
- Inject custom components to override built-in components
|
||||
|
||||
Practical examples of extensions include:
|
||||
|
||||
- A set of segmentation tools that build on top of the `cornerstone` viewport
|
||||
- Showing ML/AI report summaries for the selected study/series/image
|
||||
- Support for parsing DICOM structured reports and displaying them in a user
|
||||
friendly way
|
||||
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
|
||||
|
||||
## Concepts
|
||||
|
||||
### Extension Skeleton
|
||||
|
||||
An extension is a plain JavaScript object has an `id` property, and one or more
|
||||
"getModuleFunctions" and/or lifecycle hooks. You can read more about
|
||||
[lifecycle hooks](#lifecycle-hooks) and [modules](#modules) further down.
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'example-extension',
|
||||
|
||||
// Lifecyle
|
||||
preRegistration() { /* */ },
|
||||
// Modules
|
||||
getCommandsModule() { /* */ },
|
||||
getToolbarModule() { /* */ },
|
||||
getPanelModule() { /* */ },
|
||||
getSopClassHandler() { /* */ },
|
||||
getViewportModule() { /* */ },
|
||||
}
|
||||
```
|
||||
|
||||
### Registering an Extension
|
||||
|
||||
There are two different ways to register and configure extensions. You can
|
||||
leverage one or both strategies. Which one(s) you choose depend on your
|
||||
application's requirements. Each [module](#modules) defined by the extension
|
||||
becomes available to the core application via the `ExtensionManager`.
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
const config = {
|
||||
extensions: [
|
||||
MyFirstExtension,
|
||||
[
|
||||
MySecondExtension,
|
||||
{ /* MySecondExtensions Configuration */ },
|
||||
],
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### Runtime Extensions
|
||||
|
||||
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
|
||||
that file includes an `Extensions` key that supports an array of extensions to
|
||||
register.
|
||||
|
||||
#### Bundled Extensions
|
||||
|
||||
The `@ohif/viewer` works best when built as a "Progressive Web Application"
|
||||
(PWA). If you know the extensions your application will need, you can specify
|
||||
them at "build time" to leverage some advantaged afforded to us by modern
|
||||
tooling:
|
||||
|
||||
- Code Splitting
|
||||
- Tree Shaking
|
||||
- Dependency deduplication
|
||||
|
||||
You can update the list of bundled extensions by:
|
||||
|
||||
1. Having your `@ohif/viewer` project depend on the extension
|
||||
2. Importing and adding it to the list of extensions in the
|
||||
`<repo-root>/platform/src/index.js` entrypoint.
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
...
|
||||
|
||||
### Modules
|
||||
|
||||
There are a few different module types. Each module type allows us to extend the
|
||||
viewer in a different way, and provides a consistent API for us to do so. You
|
||||
can find a full list of the different types of modules
|
||||
[`in ohif-core`][module-types]. Information on each type of module, it's API,
|
||||
and how we determine when/where it should be used is included below.
|
||||
|
||||
### Contexts
|
||||
|
||||
...
|
||||
|
||||
## Consuming Extensions
|
||||
|
||||
...
|
||||
|
||||
## Maintained Extensions
|
||||
|
||||
A small number of powerful extensions for popular use cases are maintained by
|
||||
OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in
|
||||
the top level [`extensions/`][ext-source] directory.
|
||||
|
||||
{% include "./_maintained-extensions-table.md" %}
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[viewers-repo]: https://github.com/OHIF/Viewers
|
||||
[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions
|
||||
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,21 @@
|
||||
# Lifecylce Hook: preRegistration
|
||||
|
||||
If an extension defines the `preRegistration` lifecycle hook, it is called
|
||||
before any modules are registered to the `ExtensionManager`.
|
||||
|
||||
```js
|
||||
export default {
|
||||
id: 'MyExampleExtension',
|
||||
|
||||
preRegistration({ servicesManager, commandsManager, configuration }) {
|
||||
console.log('Wiring up important stuff.');
|
||||
|
||||
window.importantStuff = () => {
|
||||
console.log(configuration);
|
||||
};
|
||||
|
||||
console.log('Important stuff has been wired.');
|
||||
window.importantStuff();
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
# Module: Commands
|
||||
|
||||
The Commands Module allows us to register one or more commands scoped to
|
||||
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
|
||||
and any registered custom react component (like a [viewport][#] or [panel][#]).
|
||||
Here is a simple example commands module:
|
||||
|
||||
```js
|
||||
{
|
||||
getCommandsModule() {
|
||||
return {
|
||||
actions: {
|
||||
speak: ({ viewports, words }) => {
|
||||
console.log(viewports, words);
|
||||
},
|
||||
},
|
||||
definitions: {
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 }
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
context: 'ACTIVE_VIEWER::CORNERSTONE'
|
||||
},
|
||||
},
|
||||
defaultContext: 'VIEWER'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Module: Panel
|
||||
|
||||
...
|
||||
@@ -0,0 +1,3 @@
|
||||
# Module: SOP Class Handler
|
||||
|
||||
...
|
||||
@@ -0,0 +1,48 @@
|
||||
# Module: Toolbar
|
||||
|
||||
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
||||
method that returns a React Component. The component does not receive any props.
|
||||
If you want to modify or react to state, you will need to connect to the redux
|
||||
store. The given toolbar must determine its set of elements and the context of
|
||||
them. The set of elements will be listed on toolbar `definitions`.
|
||||
|
||||

|
||||
|
||||
<center><i>A toolbar extension example</i></center>
|
||||
|
||||
Toolbar components are rendered in the `ToolbarRow` component.
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
||||
|
||||
## Toolbar Custom Component
|
||||
|
||||
Toolbar elements can define its own custom react component to be consumed when
|
||||
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
|
||||
you just need to expose your `CustomToolbarComponent` as the value of key
|
||||
`CustomComponent`. In case the property `CustomComponent` is not present, a
|
||||
default toolbar component will be used to render it. See bellow
|
||||
|
||||
```js
|
||||
definitions: [
|
||||
...
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
}
|
||||
...
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
`CustomComponent` components will receive the following props:
|
||||
|
||||
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
|
||||
instance)
|
||||
- toolbarClickCallback: callback method when clicking on toolbar
|
||||
- button: its own definition object
|
||||
- key: react key prop
|
||||
- activeButtons: list of active elements
|
||||
- isActive: if current
|
||||
@@ -0,0 +1,29 @@
|
||||
# Module: Viewport
|
||||
|
||||
An extension can register a Viewport Module by providing a `getViewportModule()`
|
||||
method that returns a React Component. The React component will receive the
|
||||
following props:
|
||||
|
||||
```js
|
||||
children: PropTypes.arrayOf(PropTypes.element)
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportData: PropTypes.object, // { studies, displaySet }
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object
|
||||
```
|
||||
|
||||
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||
component is used depends on:
|
||||
|
||||
- The Layout Configuration
|
||||
- Registered SopClassHandlers
|
||||
- The SopClassUID for visible/selected datasets
|
||||
|
||||

|
||||
|
||||
<center><i>An example of three Viewports</i></center>
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
|
||||
@@ -0,0 +1,3 @@
|
||||
# Hotkeys
|
||||
|
||||
...
|
||||
@@ -1,13 +1,13 @@
|
||||
# Our Process
|
||||
|
||||
Our process is a living, breathing thing. We strive to have regular [retrospectives][retrospective] that help us
|
||||
shape and adapt our process to our team's current needs. This document attempts to capture the broad strokes of
|
||||
Our process is a living, breathing thing. We strive to have regular
|
||||
[retrospectives][retrospective] that help us shape and adapt our process to our
|
||||
team's current needs. This document attempts to capture the broad strokes of
|
||||
that process in an effort to:
|
||||
|
||||
- Strengthen community member involvement and understanding
|
||||
- Welcome feedback and helpful suggestions
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
- [Issue Triage](#issue-triage)
|
||||
@@ -15,78 +15,117 @@ that process in an effort to:
|
||||
- [Contributions (Pull Requests)](#contributions-pull-requests)
|
||||
- [Releases](#releases)
|
||||
|
||||
|
||||
_Include issue lifecycle diagram_
|
||||
|
||||
## Issue Triage
|
||||
|
||||
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions, and suggest changes to the OHIF Viewer's core
|
||||
team. Community issues generally fall into one of three categories, and are marked with a `triage` label when created.
|
||||
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions,
|
||||
and suggest changes to the OHIF Viewer's core team. Community issues generally
|
||||
fall into one of three categories, and are marked with a `triage` label when
|
||||
created.
|
||||
|
||||
|
||||
|Issue Template Name | Description |
|
||||
|------------------------|-----------------------------------------------------------------------------------------|
|
||||
|Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|
||||
|Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|
||||
|Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
|
||||
| Issue Template Name | Description |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|
||||
| Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|
||||
| Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
|
||||
|
||||
_table 1. issue template names and descriptions_
|
||||
|
||||
Issues that require `triage` are akin to support tickets. As this is often our first contact with would-be adopters and
|
||||
contributors, it's important that we strive for timely responses and satisfactory resolutions. We attempt to accomplish this
|
||||
by:
|
||||
Issues that require `triage` are akin to support tickets. As this is often our
|
||||
first contact with would-be adopters and contributors, it's important that we
|
||||
strive for timely responses and satisfactory resolutions. We attempt to
|
||||
accomplish this by:
|
||||
|
||||
1. Responding to issues requiring `triage` at least once a week
|
||||
2. Create new "official issues" from "community issues"
|
||||
3. Provide clear guidance and next steps (when applicable)
|
||||
4. Regularly clean up old (stale) issues
|
||||
|
||||
> :pencil: Less obviously, patterns in the issues being reported can highlight areas that need improvement. For example, users often have
|
||||
difficulty navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket volume for this issue?
|
||||
> :pencil: Less obviously, patterns in the issues being reported can highlight
|
||||
> areas that need improvement. For example, users often have difficulty
|
||||
> navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce
|
||||
> our ticket volume for this issue?
|
||||
|
||||
### Backlogged Issues
|
||||
|
||||
Community issues serve as vehicles of discussion that lead us to "backlogged issues". Backlogged issues are the distilled and actionable information extracted from community issues. They contain the scope and requirements necessary for hand-off to a core-team (or community) contributor ^_^
|
||||
Community issues serve as vehicles of discussion that lead us to "backlogged
|
||||
issues". Backlogged issues are the distilled and actionable information
|
||||
extracted from community issues. They contain the scope and requirements
|
||||
necessary for hand-off to a core-team (or community) contributor ^\_^
|
||||
|
||||
|Category| Description| Labels|
|
||||
|--------|-----| ---------|
|
||||
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
|
||||
| Stories| A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
|
||||
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
|
||||
| Category | Description | Labels |
|
||||
| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
|
||||
| Stories | A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
|
||||
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
|
||||
|
||||
_table 2. backlogged issue types ([full list of labels][gh-labels])_
|
||||
|
||||
|
||||
## Issue Curation (["backlog grooming"][groom-backlog])
|
||||
|
||||
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on our backlog. If an issue is on our backlog, it means we are, at the very least, committed to reviewing any community drafted Pull Requests to complete the issue. If you're interested in seeing an issue completed but don't know where to start, please don't hesitate to leave a comment!
|
||||
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on
|
||||
our backlog. If an issue is on our backlog, it means we are, at the very least,
|
||||
committed to reviewing any community drafted Pull Requests to complete the
|
||||
issue. If you're interested in seeing an issue completed but don't know where to
|
||||
start, please don't hesitate to leave a comment!
|
||||
|
||||
While we don't yet have a long-term or quarterly road map, we do regularly add items to our ["Active Development" GitHub Project Board][gh-board]. Items on this project board are either in active development by Core Team members, or queued up for development as in-progress items are completed.
|
||||
While we don't yet have a long-term or quarterly road map, we do regularly add
|
||||
items to our ["Active Development" GitHub Project Board][gh-board]. Items on
|
||||
this project board are either in active development by Core Team members, or
|
||||
queued up for development as in-progress items are completed.
|
||||
|
||||
> :pencil: Want to contribute but not sure where to start? Check out [Up for grabs][label-grabs] issues and our [Contributing documentation][contributing-docs]
|
||||
> :pencil: Want to contribute but not sure where to start? Check out [Up for
|
||||
> grabs][label-grabs] issues and our [Contributing
|
||||
> documentation][contributing-docs]
|
||||
|
||||
## Contributions (Pull Requests)
|
||||
|
||||
Incoming Pull Requests (PRs) are triaged using the following labels. Code review is performed on all PRs where the bug fix or added functionality is deemed appropriate:
|
||||
Incoming Pull Requests (PRs) are triaged using the following labels. Code review
|
||||
is performed on all PRs where the bug fix or added functionality is deemed
|
||||
appropriate:
|
||||
|
||||
- PR: Bug Fix - Describes a PR filed to address a Bug.
|
||||
- PR: Draft - Describes a PR filed to gather early feedback from the core team, but which is not intended for merging in the short term.
|
||||
- Awaiting Response - Describes a PR where the core team is waiting for additional information from the author. This may occur before or after code review.
|
||||
- Awaiting Review - Describes a PR where the core team has not yet performed a code review.
|
||||
- Awaiting Revisions - Following code review, this label is applied until the author has made sufficient changes.
|
||||
| Labels | Description |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| **Classification** | |
|
||||
| [PR: Bug Fix][label-bug] | Filed to address a Bug. |
|
||||
| [PR: Draft][draft] | Filed to gather early feedback from the core team, but which is not intended for merging in the short term. |
|
||||
| **Review Workflow** | |
|
||||
| [PR: Awaiting Response 💬][awaiting-response] | The core team is waiting for additional information from the author. |
|
||||
| [PR: Awaiting Review 👀][awaiting-review] | The core team has not yet performed a code review. |
|
||||
| [PR: Awaiting Revisions 🖊][awaiting-revisions] | Following code review, this label is applied until the author has made sufficient changes. |
|
||||
| **QA** | |
|
||||
| [PR: Awaiting User Cases 💃][awaiting-stories] | The PR code changes need common language descriptions of impact to end users before the review can start |
|
||||
| [PR: No UX Impact 🙃][no-ux-impact] | The PR code changes do not impact the user's experience |
|
||||
|
||||
We rely on GitHub Checks and integrations with third party services to evaluate changes in code quality and test coverage. Tests must pass before a PR can be merged to master, and code quality and test coverage must not changed by a significant margin. For some repositories, visual screenshot-based tests are also included, and video recordings of end-to-end tests are stored for later review.
|
||||
We rely on GitHub Checks and integrations with third party services to evaluate
|
||||
changes in code quality and test coverage. Tests must pass and User cases must
|
||||
be present (when applicable) before a PR can be merged to master, and code
|
||||
quality and test coverage must not changed by a significant margin. For some
|
||||
repositories, visual screenshot-based tests are also included, and video
|
||||
recordings of end-to-end tests are stored for later review.
|
||||
|
||||
[You can read more about our continous integration efforts here](/continous-integration.md)
|
||||
|
||||
## Releases
|
||||
|
||||
Releases are made automatically based on the type of commits which have been merged (major.minor.patch). Releases are automatically pushed to NPM. Release notes are automatically generated. Users can subscribe to GitHub and NPM releases.
|
||||
Releases are made automatically based on the type of commits which have been
|
||||
merged (major.minor.patch). Releases are automatically pushed to NPM. Release
|
||||
notes are automatically generated. Users can subscribe to GitHub and NPM
|
||||
releases.
|
||||
|
||||
Important announcements are made on GitHub, tagged as Announcement, and pinned so that they remain at the top of the Issue page.
|
||||
We host development, staging, and production environments for the Progressive
|
||||
Web Application version of the OHIF Viewer. [Development][ohif-dev] always
|
||||
reflects the latest changes on our master branch. [Staging][ohif-stage] is used
|
||||
to regression test a release before a bi-weekly deploy to our [Production
|
||||
environment][ohif-prod].
|
||||
|
||||
The Core team occasionally performs full manual testing to begin the process of releasing a Stable version. Once testing is complete, the known issues are addressed and a Stable version
|
||||
Important announcements are made on GitHub, tagged as Announcement, and pinned
|
||||
so that they remain at the top of the Issue page.
|
||||
|
||||
The Core team occasionally performs full manual testing to begin the process of
|
||||
releasing a Stable version. Once testing is complete, the known issues are
|
||||
addressed and a Stable version is released.
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
@@ -97,12 +136,24 @@ The Core team occasionally performs full manual testing to begin the process of
|
||||
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
|
||||
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
|
||||
[gh-labels]: https://github.com/OHIF/Viewers/labels
|
||||
<!-- Issue Labels -->
|
||||
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
|
||||
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
|
||||
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
|
||||
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
|
||||
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
|
||||
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
|
||||
<!-- PR Labels -->
|
||||
[draft]: https://github.com/OHIF/Viewers/labels/PR%3A%20Draft
|
||||
[awaiting-response]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Response%20%3Aspeech_balloon%3A
|
||||
[awaiting-review]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Review%20%3Aeyes%3A
|
||||
[awaiting-stories]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20UX%20Stories%20%3Adancer%3A
|
||||
[awaiting-revisions]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Revisions%20%3Apen%3A
|
||||
[no-ux-impact]: https://github.com/OHIF/Viewers/labels/PR%3A%20No%20UX%20Impact%20%3Aupside_down_face%3A
|
||||
<!-- -->
|
||||
[ohif-dev]: https://viewer-dev.ohif.org
|
||||
[ohif-stage]: https://viewer-stage.ohif.org
|
||||
[ohif-prod]: https://viewer.ohif.org
|
||||
[gh-board]: https://github.com/OHIF/Viewers/projects/4
|
||||
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
|
||||
[contributing-docs]: https://docs.ohif.org/contributing/
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Services (default)
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
|
||||
## Overview
|
||||
|
||||
Services are a work in progress. As we are still in the progress of creating a
|
||||
non-ui maintained service, this usage may change.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/services.png">
|
||||
<img src="/assets/img/services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
|
||||
</div>
|
||||
|
||||
## Example
|
||||
|
||||
The simplest service return a new object that has a `name` property, and
|
||||
methods/properties that give the service its functionality. The "Factory
|
||||
Function" that creates the service is provided with the implementation (this is
|
||||
slightly different for UI Services).
|
||||
|
||||
```js
|
||||
const _speak = () => {
|
||||
console.warn('Speak is not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function to create `HelloWorldService`
|
||||
*
|
||||
* @param {object} implementation
|
||||
* @param {function} implementation.speak - Speak's implementation
|
||||
* @returns HelloWorldService
|
||||
*/
|
||||
export default function createHelloWorldService({ speak }) {
|
||||
return {
|
||||
name: 'HelloWorldService',
|
||||
speak: speak || _speak,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
A service, once created, can be registered with the `ServicesManager` to make it
|
||||
accessible to extensions. Similarly, the application code can access named
|
||||
services from the `ServicesManager`.
|
||||
|
||||
```js
|
||||
// In the application
|
||||
const speak = () => {
|
||||
window.alert('HELLO WORLD');
|
||||
};
|
||||
const HelloWorldService = createHelloWorldService({ speak });
|
||||
const servicesManager = new ServicesManager();
|
||||
|
||||
servicesManager.registerService(HelloWorldService);
|
||||
|
||||
// In an extension
|
||||
const { HelloWorldService } = servicesManager.services;
|
||||
|
||||
if (HelloWorldService) {
|
||||
HelloWorldService.speak();
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,69 @@
|
||||
# Services Overview
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Kinds of Services](#kinds-of-services)
|
||||
- [Services (default)](#services-default)
|
||||
- [UI Services](#ui-services)
|
||||
- [Related Patterns](#related-patterns)
|
||||
|
||||
## Overview
|
||||
|
||||
Services are a beefier version of [commands][commands]. They provide a set of
|
||||
operations, often tied to some shared state, and are made available to
|
||||
extensions via the `ServicesManager`. Services are particularly well suited to
|
||||
address [cross-cutting concerns][cross-cutting-concerns].
|
||||
|
||||
Each service should be:
|
||||
|
||||
- self-contained
|
||||
- able to fail and/or be removed without breaking the application
|
||||
- completely interchangeable with another module implementing the same interface
|
||||
|
||||
## Kinds of Services
|
||||
|
||||
Depending on the kind of service, we follow slightly different conventions. For
|
||||
example, a UI service often receives its implementation from a React Context
|
||||
Provider. You can read more about the different kinds of services and what makes
|
||||
them different below:
|
||||
|
||||
### Services (default)
|
||||
|
||||
Services are a work in progress. As we are still in the progress of creating a
|
||||
non-ui maintained service, this usage may change.
|
||||
|
||||
[You can read more about default services: here](./default/index.md)
|
||||
|
||||
### UI Services
|
||||
|
||||
A typical web application will have components and state for common UI like
|
||||
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
|
||||
these components from an extension.
|
||||
|
||||
We maintain the following UI Services:
|
||||
|
||||
- [UIDialogService](./ui/ui-dialog-service.md)
|
||||
- [UIModalService](./ui/ui-modal-service.md)
|
||||
- [UINotificationService](./ui/ui-notification-service.md)
|
||||
|
||||
You can read more about a specific service by selecting it in the above list,
|
||||
and more about [UI services in general: here](./ui/index.md)
|
||||
|
||||
## Related Patterns
|
||||
|
||||
Services are "concern-specific" code modules that can be consumed across layers.
|
||||
We try to minimize the coupling they introduce by authoring services that are
|
||||
able to fail or be removed. Related patterns that may reduce coupling include:
|
||||
|
||||
- Pub/Sub
|
||||
- Commands
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[commands]: #/
|
||||
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
|
||||
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
|
||||
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,106 @@
|
||||
# UI Services
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Tips & Tricks](#/)
|
||||
- [Maintained Services](#/)
|
||||
|
||||
## Overview
|
||||
|
||||
A typical web application will have components and state for common UI like
|
||||
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
|
||||
these components from an extension.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/ui-services.png">
|
||||
<img src="/assets/img/ui-services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
|
||||
</div>
|
||||
|
||||
In `@ohif/core`, we have a collection of service factories. We select one we
|
||||
would like our application to support, create an instance of it, and pass that
|
||||
instance to our `ServicesManager` AND to a React component (in this example,
|
||||
`ModalContext`'s provider).
|
||||
|
||||
The `ModalContext`'s provider:
|
||||
|
||||
- Exposes context values
|
||||
- Exposes methods that leverage `useCallback` hooks
|
||||
- Sets the service's implementation in a `useEffect` hook
|
||||
|
||||
The `ServicesManager` is:
|
||||
|
||||
- Passed to the `ExtensionManager`
|
||||
- The `ExtensionManager` makes the `ServicesManager` available to:
|
||||
- All of it's lifecycle hooks (`preRegistration`)
|
||||
- Each "getModuleFunction" (`getToolbarModule`, `getPanelModule`, etc.)
|
||||
|
||||
## Example
|
||||
|
||||
That's all fine and good, but it's still a little too abstract. What does this
|
||||
translate to in practice?
|
||||
|
||||
```js
|
||||
// In the application
|
||||
const UINotificationService = createUINotificationService();
|
||||
const servicesManager = new ServicesManager();
|
||||
|
||||
servicesManager.registerService(UINotificationService);
|
||||
|
||||
// UI Service Provider
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({ hide, show });
|
||||
}
|
||||
}, [service, hide, show]);
|
||||
|
||||
// In an extension
|
||||
const { UINotificationService } = servicesManager.services;
|
||||
|
||||
if (UINotificationService) {
|
||||
UINotificationService.show('Hello from the other side 👋');
|
||||
}
|
||||
```
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/notification-example.gif">
|
||||
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
|
||||
</div>
|
||||
|
||||
## Tips & Tricks
|
||||
|
||||
It's important to remember that all we're doing is making it possible to control
|
||||
bits of the application's UI from an extension. Here are a few non-obvious
|
||||
takeaways worth mentioning:
|
||||
|
||||
- Your application code should continue to use React context
|
||||
(consumers/providers) as it normally would
|
||||
- You can substitute our "out of the box" UI implementations with your own
|
||||
- You can create and register your own UI services
|
||||
- You can choose not to register a service or provide a service implementation
|
||||
- In extensions, you can provide fallback/alternative behavior if an expected
|
||||
service is not registered
|
||||
- No `UIModalService`? Use the `UINotificationService` to notify users.
|
||||
- While we don't have an examples of this, you can technically register a
|
||||
service in an extension and expose it to the core application
|
||||
|
||||
> Note: These are recommended patterns, not hard and fast rules. Following them
|
||||
> will help reduce confusion and interoperability with the larger OHIF
|
||||
> community, but they're not silver bullets. Please speak up, create an issue,
|
||||
> if you would like to discuss new services or improvements to this pattern.
|
||||
|
||||
## Maintained Services
|
||||
|
||||
Our `@ohif/viewer` project is an example of how to glue together the different
|
||||
parts and pieces of the OHIF Platform to create a polished and powerful product.
|
||||
To accomplish that, we maintain several UI Services that you can use in your own
|
||||
project, or provide alternative implementations for:
|
||||
|
||||
| Name | Docs |
|
||||
| --------------------- | ------------------------------------ |
|
||||
| UIDialogService | [Here](./ui-dialog-service.md) |
|
||||
| UIModalService | [Here](./ui-modal-service.md) |
|
||||
| UINotificationService | [Here](./ui-notification-service.md) |
|
||||
@@ -0,0 +1,49 @@
|
||||
# UI Dialog Service
|
||||
|
||||
Dialogs have similar characteristics to that of Modals, but often with a
|
||||
streamlined focus. They can be helpful when:
|
||||
|
||||
- We need to grab the user's attention
|
||||
- We need user input
|
||||
- We need to show additional information
|
||||
|
||||
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
|
||||
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/dialog-example.gif">
|
||||
<img src="/assets/img/dialog-example.gif" alt="UI Dialog Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>GIF showing successful call of UIDialogService from an extension.</i></div>
|
||||
</div>
|
||||
|
||||
## Interface
|
||||
|
||||
For a more detailed look on the options and return values each of these methods
|
||||
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||
|
||||
| API Member | Description |
|
||||
| -------------- | ------------------------------------------------------ |
|
||||
| `create()` | Creates a new Dialog that is displayed until dismissed |
|
||||
| `dismiss()` | Dismisses the specified dialog |
|
||||
| `dismissAll()` | Dismisses all dialogs |
|
||||
|
||||
## Implementations
|
||||
|
||||
| Implementation | Consumer |
|
||||
| ------------------------------------ | -------------------------- |
|
||||
| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider |
|
||||
|
||||
`*` - Denotes maintained by OHIF
|
||||
|
||||
> 3rd Party implementers may be added to this table via pull requests.
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js
|
||||
[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js
|
||||
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,50 @@
|
||||
# UI Modal Service
|
||||
|
||||
Modals have similar characteristics to that of Dialogs, but are often larger,
|
||||
and only allow for a single instance to be viewable at once. They also tend to
|
||||
be centered, and not draggable. They're commonly used when:
|
||||
|
||||
- We need to grab the user's attention
|
||||
- We need user input
|
||||
- We need to show additional information
|
||||
|
||||
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
|
||||
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/modal-example.gif">
|
||||
<img src="/assets/img/modal-example.gif" alt="UI Modal Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>GIF showing successful call of UIModalService from an extension.</i></div>
|
||||
</div>
|
||||
|
||||
## Interface
|
||||
|
||||
For a more detailed look on the options and return values each of these methods
|
||||
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||
|
||||
| API Member | Description |
|
||||
| ---------- | ------------------------------------- |
|
||||
| `hide()` | Hides the open modal |
|
||||
| `show()` | Shows the provided content in a modal |
|
||||
|
||||
## Implementations
|
||||
|
||||
| Implementation | Consumer |
|
||||
| ---------------------------------- | ----------------------------- |
|
||||
| [Modal Provider][modal-provider]\* | [OHIFModal][modal-consumer]\* |
|
||||
|
||||
`*` - Denotes maintained by OHIF
|
||||
|
||||
> 3rd Party implementers may be added to this table via pull requests.
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
|
||||
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
|
||||
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
|
||||
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,51 @@
|
||||
# UI Notification Service
|
||||
|
||||
Notifications can be annoying and disruptive. They can also deliver timely
|
||||
helpful information, or expedite the user's workflow. Here is some high level
|
||||
guidance on when and how to use them:
|
||||
|
||||
- Notifications should be non-interfering (timely, relevant, important)
|
||||
- We should only show small/brief notifications
|
||||
- Notifications should be contextual to current behavior/actions
|
||||
- Notifications can serve warnings (acting as a confirmation)
|
||||
|
||||
If you're curious about the DOs and DON'Ts of notifications, check out this
|
||||
article: ["How To Design Notifications For Better UX"][ux-article]
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/notification-example.gif">
|
||||
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
|
||||
</div>
|
||||
|
||||
## Interface
|
||||
|
||||
For a more detailed look on the options and return values each of these methods
|
||||
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
||||
|
||||
| API Member | Description |
|
||||
| ---------- | --------------------------------------- |
|
||||
| `hide()` | Hides the specified notification |
|
||||
| `show()` | Creates and displays a new notification |
|
||||
|
||||
## Implementations
|
||||
|
||||
| Implementation | Consumer |
|
||||
| ---------------------------------------- | ----------------------------------------- |
|
||||
| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* |
|
||||
|
||||
`*` - Denotes maintained by OHIF
|
||||
|
||||
> 3rd Party implementers may be added to this table via pull requests.
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js
|
||||
[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js
|
||||
[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js
|
||||
[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.7.1",
|
||||
"version": "1.7.2",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from 'react';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ViewportDownloadForm } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import { getEnabledElement } from './state';
|
||||
|
||||
const MINIMUM_SIZE = 100;
|
||||
const DEFAULT_SIZE = 512;
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
|
||||
const CornerstoneViewportDownloadForm = ({ onClose, activeViewportIndex }) => {
|
||||
const activeEnabledElement = getEnabledElement(activeViewportIndex);
|
||||
|
||||
const enableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
cornerstone.enable(viewportElement);
|
||||
}
|
||||
};
|
||||
|
||||
const disableViewport = viewportElement => {
|
||||
if (viewportElement) {
|
||||
cornerstone.disable(viewportElement);
|
||||
}
|
||||
};
|
||||
|
||||
const updateViewportPreview = (viewportElement, downloadCanvas, fileType) =>
|
||||
new Promise(resolve => {
|
||||
cornerstone.fitToWindow(viewportElement);
|
||||
|
||||
viewportElement.addEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
function updateViewport(event) {
|
||||
const enabledElement = cornerstone.getEnabledElement(event.target)
|
||||
.element;
|
||||
const type = 'image/' + fileType;
|
||||
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
||||
|
||||
let newWidth = enabledElement.offsetHeight;
|
||||
let newHeight = enabledElement.offsetWidth;
|
||||
|
||||
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
||||
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
||||
newHeight *= multiplier;
|
||||
newWidth *= multiplier;
|
||||
}
|
||||
|
||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||
|
||||
viewportElement.removeEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
updateViewport
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const loadImage = (activeViewport, viewportElement, width, height) =>
|
||||
new Promise(resolve => {
|
||||
if (activeViewport && viewportElement) {
|
||||
const enabledElement = cornerstone.getEnabledElement(activeViewport);
|
||||
const viewport = Object.assign({}, enabledElement.viewport);
|
||||
delete viewport.scale;
|
||||
viewport.translation = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
|
||||
cornerstone.displayImage(viewportElement, image);
|
||||
cornerstone.setViewport(viewportElement, viewport);
|
||||
cornerstone.resize(viewportElement, true);
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(height || image.height, MAX_TEXTURE_SIZE);
|
||||
|
||||
resolve({ image, width: newWidth, height: newHeight });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const toggleAnnotations = (toggle, viewportElement) => {
|
||||
cornerstoneTools.store.state.tools.forEach(({ name }) => {
|
||||
if (toggle) {
|
||||
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
|
||||
} else {
|
||||
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const downloadBlob = (
|
||||
filename,
|
||||
fileType,
|
||||
viewportElement,
|
||||
downloadCanvas
|
||||
) => {
|
||||
const file = `${filename}.${fileType}`;
|
||||
const mimetype = `image/${fileType}`;
|
||||
|
||||
/* Handles JPEG images for IE11 */
|
||||
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
|
||||
const image = downloadCanvas.toDataURL(mimetype, 1);
|
||||
const blob = utils.b64toBlob(
|
||||
image.replace('data:image/jpeg;base64,', ''),
|
||||
mimetype
|
||||
);
|
||||
return window.navigator.msSaveBlob(blob, file);
|
||||
}
|
||||
|
||||
viewportElement.querySelector('canvas').toBlob(blob => {
|
||||
const URLObj = window.URL || window.webkitURL;
|
||||
const a = document.createElement('a');
|
||||
a.href = URLObj.createObjectURL(blob);
|
||||
a.download = file;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewportDownloadForm
|
||||
onClose={onClose}
|
||||
minimumSize={MINIMUM_SIZE}
|
||||
maximumSize={MAX_TEXTURE_SIZE}
|
||||
defaultSize={DEFAULT_SIZE}
|
||||
canvasClass={'cornerstone-canvas'}
|
||||
activeViewport={activeEnabledElement}
|
||||
enableViewport={enableViewport}
|
||||
disableViewport={disableViewport}
|
||||
updateViewportPreview={updateViewportPreview}
|
||||
loadImage={loadImage}
|
||||
toggleAnnotations={toggleAnnotations}
|
||||
downloadBlob={downloadBlob}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CornerstoneViewportDownloadForm.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default CornerstoneViewportDownloadForm;
|
||||
@@ -1,233 +1,260 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import { getEnabledElement } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.rotation += rotation;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
flipViewportHorizontal: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.hflip = !viewport.hflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
flipViewportVertical: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.vflip = !viewport.vflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
scaleViewport: ({ direction, viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
const step = direction * 0.15;
|
||||
|
||||
if (enabledElement) {
|
||||
if (step) {
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.scale += step;
|
||||
viewport.rotation += rotation;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
} else {
|
||||
cornerstone.fitToWindow(enabledElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetViewport: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
},
|
||||
flipViewportHorizontal: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
cornerstone.reset(enabledElement);
|
||||
}
|
||||
},
|
||||
invertViewport: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.hflip = !viewport.hflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
flipViewportVertical: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
||||
// better mouseButtonMask sets.
|
||||
setToolActive: ({ toolName }) => {
|
||||
if (!toolName) {
|
||||
console.warn('No toolname provided to setToolActive command');
|
||||
}
|
||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||
},
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.vflip = !viewport.vflip;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
scaleViewport: ({ direction, viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
const step = direction * 0.15;
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
toolState,
|
||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
if (
|
||||
!toolState ||
|
||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||
|
||||
const measurementsToRemove = [];
|
||||
|
||||
Object.keys(imageIdToolState).forEach(toolType => {
|
||||
const { data } = imageIdToolState[toolType];
|
||||
|
||||
data.forEach(measurementData => {
|
||||
const { _id, lesionNamingNumber, measurementNumber } = measurementData;
|
||||
if (!_id) {
|
||||
return;
|
||||
if (enabledElement) {
|
||||
if (step) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.scale += step;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
} else {
|
||||
cornerstone.fitToWindow(enabledElement);
|
||||
}
|
||||
}
|
||||
},
|
||||
resetViewport: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
measurementsToRemove.push({
|
||||
toolType,
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
if (enabledElement) {
|
||||
cornerstone.reset(enabledElement);
|
||||
}
|
||||
},
|
||||
invertViewport: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
viewport.invert = !viewport.invert;
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
// TODO: this is receiving `evt` from `ToolbarRow`. We could use it to have
|
||||
// better mouseButtonMask sets.
|
||||
setToolActive: ({ toolName }) => {
|
||||
if (!toolName) {
|
||||
console.warn('No toolname provided to setToolActive command');
|
||||
}
|
||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||
},
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
toolState,
|
||||
} = cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
if (
|
||||
!toolState ||
|
||||
toolState.hasOwnProperty(enabledElement.image.imageId) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[enabledElement.image.imageId];
|
||||
|
||||
const measurementsToRemove = [];
|
||||
|
||||
Object.keys(imageIdToolState).forEach(toolType => {
|
||||
const { data } = imageIdToolState[toolType];
|
||||
|
||||
data.forEach(measurementData => {
|
||||
const {
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
} = measurementData;
|
||||
if (!_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurementsToRemove.push({
|
||||
toolType,
|
||||
_id,
|
||||
lesionNamingNumber,
|
||||
measurementNumber,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
measurementsToRemove.forEach(measurementData => {
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData,
|
||||
measurementsToRemove.forEach(measurementData => {
|
||||
OHIF.measurements.MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
nextImage: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, 1);
|
||||
},
|
||||
previousImage: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, -1);
|
||||
},
|
||||
getActiveViewportEnabledElement: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
return enabledElement;
|
||||
},
|
||||
showDownloadViewportModal: ({ title, viewports }) => {
|
||||
const activeViewportIndex = viewports.activeViewportIndex;
|
||||
const { UIModalService } = servicesManager.services;
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
nextImage: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, 1);
|
||||
},
|
||||
previousImage: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, -1);
|
||||
},
|
||||
getActiveViewportEnabledElement: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
return enabledElement;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
getActiveViewportEnabledElement: {
|
||||
commandFn: actions.getActiveViewportEnabledElement,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 },
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportVertical: {
|
||||
commandFn: actions.flipViewportVertical,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
scaleUpViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
scaleDownViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
fitViewportToWindow: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 0 },
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
clearAnnotations: {
|
||||
commandFn: actions.clearAnnotations,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
nextImage: {
|
||||
commandFn: actions.nextImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
previousImage: {
|
||||
commandFn: actions.previousImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
nextViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
};
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
getActiveViewportEnabledElement: {
|
||||
commandFn: actions.getActiveViewportEnabledElement,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 },
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
invertViewport: {
|
||||
commandFn: actions.invertViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportVertical: {
|
||||
commandFn: actions.flipViewportVertical,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
scaleUpViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
scaleDownViewport: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
fitViewportToWindow: {
|
||||
commandFn: actions.scaleViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { direction: 0 },
|
||||
},
|
||||
resetViewport: {
|
||||
commandFn: actions.resetViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
clearAnnotations: {
|
||||
commandFn: actions.clearAnnotations,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
nextImage: {
|
||||
commandFn: actions.nextImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
previousImage: {
|
||||
commandFn: actions.previousImage,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
nextViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
};
|
||||
export default commandsModule;
|
||||
@@ -2,6 +2,7 @@ import init from './init.js';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
|
||||
const OHIFCornerstoneViewport = asyncComponent(() =>
|
||||
import(
|
||||
@@ -33,7 +34,9 @@ export default {
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
getCommandsModule() {
|
||||
return commandsModule;
|
||||
getCommandsModule({ servicesManager }) {
|
||||
return commandsModule({ servicesManager });
|
||||
},
|
||||
};
|
||||
|
||||
export { CornerstoneViewportDownloadForm };
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.54.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.2...@ohif/extension-vtk@0.54.3) (2019-12-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.1...@ohif/extension-vtk@0.54.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.54.2",
|
||||
"version": "0.54.3",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -52,7 +52,7 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.13.2",
|
||||
"@ohif/core": "^1.13.3",
|
||||
"@ohif/ui": "^0.65.1",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.1...@ohif/core@1.13.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.13.2",
|
||||
"version": "1.13.3",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* @property {ReactElement|HTMLElement} content The dialog content.
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.11.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.4...@ohif/viewer@2.11.5) (2019-12-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.11.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.3...@ohif/viewer@2.11.4) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.11.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.2...@ohif/viewer@2.11.3) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "2.11.3",
|
||||
"version": "2.11.5",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -45,12 +45,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^1.13.2",
|
||||
"@ohif/core": "^1.13.3",
|
||||
"@ohif/extension-cornerstone": "^2.0.0",
|
||||
"@ohif/extension-dicom-html": "^1.0.2",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||
"@ohif/extension-vtk": "^0.54.2",
|
||||
"@ohif/extension-vtk": "^0.54.3",
|
||||
"@ohif/i18n": "^0.52.2",
|
||||
"@ohif/ui": "^0.65.1",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { ViewportDownloadForm } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import { commandsManager } from './../App.js';
|
||||
|
||||
const MINIMUM_SIZE = 100;
|
||||
const DEFAULT_SIZE = 512;
|
||||
const MAX_TEXTURE_SIZE = 10000;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const activeEnabledElement = commandsManager.runCommand(
|
||||
'getActiveViewportEnabledElement'
|
||||
);
|
||||
|
||||
return {
|
||||
onClose: ownProps.hide,
|
||||
minimumSize: MINIMUM_SIZE,
|
||||
maximumSize: MAX_TEXTURE_SIZE,
|
||||
defaultSize: DEFAULT_SIZE,
|
||||
canvasClass: 'cornerstone-canvas',
|
||||
activeViewport: activeEnabledElement,
|
||||
enableViewport: viewportElement => {
|
||||
if (viewportElement) {
|
||||
cornerstone.enable(viewportElement);
|
||||
}
|
||||
},
|
||||
disableViewport: viewportElement => {
|
||||
if (viewportElement) {
|
||||
cornerstone.disable(viewportElement);
|
||||
}
|
||||
},
|
||||
updateViewportPreview: (viewportElement, downloadCanvas, fileType) =>
|
||||
new Promise(resolve => {
|
||||
cornerstone.fitToWindow(viewportElement);
|
||||
|
||||
viewportElement.addEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
function updateViewport(event) {
|
||||
const enabledElement = cornerstone.getEnabledElement(event.target)
|
||||
.element;
|
||||
const type = 'image/' + fileType;
|
||||
const dataUrl = downloadCanvas.toDataURL(type, 1);
|
||||
|
||||
let newWidth = enabledElement.offsetHeight;
|
||||
let newHeight = enabledElement.offsetWidth;
|
||||
|
||||
if (newWidth > DEFAULT_SIZE || newHeight > DEFAULT_SIZE) {
|
||||
const multiplier = DEFAULT_SIZE / Math.max(newWidth, newHeight);
|
||||
newHeight *= multiplier;
|
||||
newWidth *= multiplier;
|
||||
}
|
||||
|
||||
resolve({ dataUrl, width: newWidth, height: newHeight });
|
||||
|
||||
viewportElement.removeEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
updateViewport
|
||||
);
|
||||
}
|
||||
);
|
||||
}),
|
||||
loadImage: (activeViewport, viewportElement, width, height) =>
|
||||
new Promise(resolve => {
|
||||
if (activeViewport && viewportElement) {
|
||||
const enabledElement = cornerstone.getEnabledElement(activeViewport);
|
||||
const viewport = Object.assign({}, enabledElement.viewport);
|
||||
delete viewport.scale;
|
||||
viewport.translation = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
cornerstone.loadImage(enabledElement.image.imageId).then(image => {
|
||||
cornerstone.displayImage(viewportElement, image);
|
||||
cornerstone.setViewport(viewportElement, viewport);
|
||||
cornerstone.resize(viewportElement, true);
|
||||
|
||||
const newWidth = Math.min(width || image.width, MAX_TEXTURE_SIZE);
|
||||
const newHeight = Math.min(
|
||||
height || image.height,
|
||||
MAX_TEXTURE_SIZE
|
||||
);
|
||||
|
||||
resolve({ image, width: newWidth, height: newHeight });
|
||||
});
|
||||
}
|
||||
}),
|
||||
toggleAnnotations: (toggle, viewportElement) => {
|
||||
cornerstoneTools.store.state.tools.forEach(({ name }) => {
|
||||
if (toggle) {
|
||||
cornerstoneTools.setToolEnabledForElement(viewportElement, name);
|
||||
} else {
|
||||
cornerstoneTools.setToolDisabledForElement(viewportElement, name);
|
||||
}
|
||||
});
|
||||
},
|
||||
downloadBlob: (filename, fileType, viewportElement, downloadCanvas) => {
|
||||
const file = `${filename}.${fileType}`;
|
||||
const mimetype = `image/${fileType}`;
|
||||
|
||||
/* Handles JPEG images for IE11 */
|
||||
if (downloadCanvas.msToBlob && fileType === 'jpeg') {
|
||||
const image = downloadCanvas.toDataURL(mimetype, 1);
|
||||
const blob = utils.b64toBlob(
|
||||
image.replace('data:image/jpeg;base64,', ''),
|
||||
mimetype
|
||||
);
|
||||
return window.navigator.msSaveBlob(blob, file);
|
||||
}
|
||||
|
||||
viewportElement.querySelector('canvas').toBlob(blob => {
|
||||
const URLObj = window.URL || window.webkitURL;
|
||||
const a = document.createElement('a');
|
||||
a.href = URLObj.createObjectURL(blob);
|
||||
a.download = file;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewportDownloadForm = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(ViewportDownloadForm);
|
||||
|
||||
export default ConnectedViewportDownloadForm;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import {
|
||||
ExpandableToolMenu,
|
||||
RoundedButtonGroup,
|
||||
@@ -14,7 +15,6 @@ import './ToolbarRow.css';
|
||||
import { commandsManager, extensionManager } from './../App.js';
|
||||
|
||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
||||
import ConnectedViewportDownloadForm from './ConnectedViewportDownloadForm';
|
||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
|
||||
|
||||
@@ -308,8 +308,7 @@ function _handleBuiltIn(button) {
|
||||
}
|
||||
|
||||
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
|
||||
modal.show({
|
||||
content: ConnectedViewportDownloadForm,
|
||||
commandsManager.runCommand('showDownloadViewportModal', {
|
||||
title: t('Download High Quality Image'),
|
||||
});
|
||||
}
|
||||
|
||||