Compare commits

...
Author SHA1 Message Date
ohif-bot 17447baf44 chore(release): publish [skip ci]
- @ohif/extension-vtk@1.0.2
 - @ohif/core@2.0.2
 - @ohif/viewer@3.0.2
2019-12-11 03:31:18 +00:00
Igor Octaviano daa3ddccdb refactor: 💡 ServicesManager factory functions vs instances (#1272)
* refactor: 💡 ServicesManager factory functions vs instances

Refactor servicesManager to take factory functions instead of instances

Closes: #1265

* Remove testing code
2019-12-10 22:28:23 -05:00
Danny Brown 29bac8df7a chore: don't mark CI/Tooling tasks as stale 2019-12-10 11:37:39 -05:00
Danny Brown 319c67c389 docs: swap out "always red" badge for viewer-dev.ohif.org badge 2019-12-10 08:57:03 -05:00
Danny Brown b9836f6c71 docs: templates in P_R_T directory can only be set w/ query string; removing 2019-12-09 22:06:01 -05:00
Danny Brown 6a6e2d7df3 docs: shift pr template up a dir 2019-12-09 22:05:05 -05:00
Danny Brown 2f8c408ed2 docs: contributing guidelines 2019-12-09 22:02:29 -05:00
Danny Brown 27910239e0 docs: code of conduct 2019-12-09 22:00:17 -05:00
ohif-bot 719bfd02eb chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.1
 - @ohif/extension-vtk@1.0.1
 - @ohif/core@2.0.1
 - @ohif/ui@1.0.1
 - @ohif/viewer@3.0.1
2019-12-09 18:58:37 +00:00
dannyrb 994818faeb Merge branch 'master' of github.com:OHIF/Viewers into ups-master 2019-12-09 13:55:04 -05:00
dannyrb f06ef53e83 docs: beginnings of SOPClassHandlerModule docs 2019-12-09 13:53:35 -05:00
ohif-bot c7008621e4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@2.0.0
 - @ohif/extension-vtk@1.0.0
 - @ohif/core@2.0.0
 - @ohif/ui@1.0.0
 - @ohif/viewer@3.0.0
2019-12-09 17:49:51 +00:00
Igor Octaviano 55a580659e feat!: Ability to configure cornerstone tools via extension configuration (#1229)
* Fix ExtensionManager bug and add test to bandaid

* Add tools configuration to extension manager preinit

* Fix reducing of configs

* Merge internal with external configs

* Merge internal with external configs

* Remove dialog from init in measurementstable

* Testing injected configuration

* New way to set config

* Add new prop to dialog provider to allow disabling last position

* Remove code from preinit in cornerstone

* Add new prop to dialog provider to allow disabling last position

* Add centralize to dialogs

* Reorder dialogs when adding them

* Fix draggable styles (cursor)

* Remove repositioning methods from labelling flow and remove overlay from labelling manager

* Fix empty array being set in bringToFront

* Add new command to update table and pass commands manager to modules/preinit hook

* Ad UIContextMenu service / factory

* Use new contextmenu service in measurementspanel extension

* Use dialogs for arrow annotate in default

* Remove positioning funcionality from tool context menu

* Add context menu service

* Pass commandsModule to extension

* Update edit description dialog and simple dialog to position relative

* Remove style code from labelling flow and manager

* Remove eventdata from labelling

* Remove labelling code from measurement init

* Add commandsmanager to provider

* Update contextmenu provider and service

* Add touchstart and mouseclick to hide contextmenu

* Hide labelling if click/touch

* Remove labelling and context menu dead code

* Fix undefined bug if ViewerMain grid has no children

* Fix broken prop on context menu

* Update commandsmodule based on master

* Fix broken configuration

* Update script tag config

* Remove cornerstone from toolcontextmenu

* Remove cornerstone from toolcontextmenu

* Split labelling and context menu providers

* Split labelling and context menu providers

* Update test

* Destructure extensions into new array

* CR Update: Move default arrow config to cornerstone instead of default

* CR Update: Fix app configuration props structure

* CR Update: Fix app configuration prop in script tag and extract commands manager from providers

* CR Update: Create custom providers to use commandsManager

* CR Update: Use services directly in measurementspanel

* CR Update: Pass components to providers

* CR Update: Remove position from dialog

* CR Update: fix dialog prop check

* CR Update: Fix comments

* CR Update: Update documentation

* CR Update: Add test default configuration

* CR Update: Add default empy array to extensions

* CR Update: Update i18n configuration all ot match current function configuration

* CR Update: Add defaults to injected dependencies in configuration and extension configuration

* CR Update: Add defaults to configuration with no args

* Update documentation

* CR Update: Add default for tools

* CR Update: Update config object to i18n

* CR Update: spread defaults

* CR Update: Add tool configuration example to cornerstone extension

* CR Update: Add tool configuration to netlify (testing)

* CR Update: Remove netlify config for tools

* CR Update: Rollback changes to i18n to be fixed later

* CR Update: Update documentation and pass whole cornerstone config object instead of tools key

SEE: https://www.conventionalcommits.org/en/v1.0.0/#commit-message-with-both-and-breaking-change-footer

BREAKING CHANGE: modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
2019-12-09 12:47:23 -05:00
dannyrb fd7620ced9 docs: add command behaviors to disambiguate situations in which commands may behave unexpectedly 2019-12-09 10:15:06 -05:00
dannyrb cd1cbf0f85 docs: fix GitHub table formatting issue 2019-12-09 09:56:17 -05:00
dannyrb 08055398de docs: commands module docs 2019-12-09 09:52:13 -05:00
dannyrb f6db7fade6 docs: basic viewport docs 2019-12-09 01:02:02 -05:00
dannyrb 58d2bf1d12 test: consistent pre-amble for panel docs 2019-12-09 00:44:43 -05:00
dannyrb 87f5eca659 docs: gif of nested buttons 2019-12-09 00:44:26 -05:00
dannyrb 922ba8cc42 docs: toolbar docs for three different kinds of definitions 2019-12-09 00:44:04 -05:00
dannyrb 777ff88f3d docs: fix incorrect paths 2019-12-08 21:31:00 -05:00
dannyrb 68858db4b6 docs: fixing jsdoc for preregistration example 2019-12-08 21:26:54 -05:00
dannyrb ab106afae3 docs: panel docs and gif 2019-12-08 21:26:39 -05:00
dannyrb ac69b0a656 docs: ExtensionManager class docs 2019-12-08 14:37:54 -05:00
dannyrb 8edba24eec docs: filling in preRegistration hook info 2019-12-08 14:37:42 -05:00
dannyrb aae232fc3e docs: tidying up extensions summary info 2019-12-08 14:09:42 -05:00
ohif-bot 8fad8ceb80 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.54.6
 - @ohif/ui@0.65.4
 - @ohif/viewer@2.11.8
2019-12-07 06:23:21 +00:00
dannyrb f00f48da2b docs: fix broken links 2019-12-07 01:21:23 -05:00
dannyrb 076b89ef83 docs: stub bulk of missing extension docs 2019-12-07 01:20:09 -05:00
dannyrb 4920b527d1 docs: fix broken link 2019-12-07 01:13:33 -05:00
dannyrb 482041f053 docs: modules and contexts summary 2019-12-07 01:13:20 -05:00
dannyrb 674edf85f0 Merge branch 'master' of github.com:OHIF/Viewers into ups-master 2019-12-07 00:49:45 -05:00
dannyrb 33a1a465ea docs: lifecycle hooks summary 2019-12-07 00:49:40 -05:00
97 changed files with 2462 additions and 1472 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ daysUntilClose: 7
exemptLabels:
- 'Story :raised_hands:'
- 'Bug: Verified :bug:'
- 'Task: CI/Tooling 🤖'
- 'Task: CI/Tooling :robot:'
- 'Task: Docs 📖'
- 'Task: Docs :book:'
- 'Task: Refactor 🛠'
+76
View File
@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
+1
View File
@@ -0,0 +1 @@
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)
+2 -2
View File
@@ -271,8 +271,8 @@ MIT © [OHIF](https://github.com/OHIF)
<!-- Badges -->
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
[lerna-url]: https://lerna.js.org/
[netlify-image]: https://api.netlify.com/api/v1/badges/a5d369ab-18a6-41c3-bcde-83805205ac7f/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif/deploys
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
+3 -2
View File
@@ -26,8 +26,9 @@
- [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)
- [Contexts](extensions/index.md#contexts)
- [ExtensionManager](extensions/index.md#extensionmanager)
- [OHIF Maintained](extensions/index.md#maintained-extensions)
- [Services](services/index.md)
- [Default](services/default/index.md)
- [Measurements](services/default/measurements.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

+46
View File
@@ -66,6 +66,52 @@ window.config = {
};
```
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
```js
window.config = ({ servicesManager } = {}) => {
const { UIDialogService } = servicesManager.services;
return {
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
}
}
},
},
routerBasename: '/',
servers: {
dicomWeb: [
{
name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
},
],
},
};
};
```
You can also create a new config file and specify its path relative to the build
output's root by setting the `APP_CONFIG` environment variable. You can set the
value of this environment variable a few different ways:
- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
- Previous `react-scripts` functionality that we need to duplicate with
`dotenv-webpack`
- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
- Previous `react-scripts` functionality that we need to duplicate with
`dotenv-webpack`
- Using the `cross-env` package in an npm script:
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
After updating the configuration, `yarn run build` to generate updated build
output.
@@ -24,12 +24,12 @@ include tags. Here's how it works:
</ul>
<ol start="2">
<li>Create a JS Object to hold the OHIF Viewer's configuration. Here are some
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
example values that would allow the viewer to hit our public PACS:</li>
</ol>
```js
// Set before importing `ohif-viewer`
// Set before importing `ohif-viewer` (JS Object)
window.config = {
// default: '/'
routerBasename: '/',
@@ -49,6 +49,9 @@ window.config = {
};
```
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](./index.md).
<ol start="3"><li>
Render the viewer in the web page's target <code>div</code>
</li></ol>
+1 -1
View File
@@ -75,7 +75,7 @@ You should see the following output:
### Building for Production
> More comprehensive guides for building and publishing can be found in our
> [deployment docs](./deployment/index.md)
> [deployment docs](./../deployment/index.md)
```bash
# Build static assets to host a PWA
+104 -22
View File
@@ -8,12 +8,15 @@
- [Modules](#modules)
- [Contexts](#contexts)
- [Consuming Extensions](#consuming-extensions)
- [Extension Manager](#extensionmanager)
- [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.
Extensions provide functionality, ui components, and new behaviors. Ideally,
they're built in a way that allows them to extend entirely different
implementations of the `@ohif/viewer` project.
<div style="text-align: center;">
<a href="/assets/img/extensions-diagram.png">
@@ -46,9 +49,8 @@ Practical examples of extensions include:
### 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.
An extension is a plain JavaScript object that has an `id` property, and one or
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
```js
// prettier-ignore
@@ -78,6 +80,12 @@ 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`.
#### Registering at Runtime
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.
```js
// prettier-ignore
const config = {
@@ -91,20 +99,13 @@ const config = {
}
```
#### 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
#### Registering at Build Time
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:
them at "build time" to leverage advantages afforded to us by modern tooling:
- Code Splitting
- Code Splitting (dynamic imports)
- Tree Shaking
- Dependency deduplication
@@ -116,23 +117,104 @@ You can update the list of bundled extensions by:
### Lifecycle Hooks
...
Currently, there is only a single lifecycle hook for extensions:
[`preRegistration`](./lifecycle/pre-registration.md)
If an extension defines the [`preRegistration`](./lifecycle/pre-registration.md)
lifecycle hook, it is called before any modules are registered in the
`ExtensionManager`. It's most commonly used to wire up extensions to
[services](./../services/index.md) and [commands](./modules/commands.md), and to
bootstrap 3rd party libraries.
### 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.
Modules are the meat of extensions. They provide "definitions", components, and
filtering/mapping logic that are then made available by various managers and
services.
Each module type has a special purpose, and is consumed by our viewer
differently.
| Type | Description | Examples |
| ------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------- |
| [Commands](./modules/commands.md) | Adds named commands, scoped to a context, to the CommandsManager | `setToolActive()`, `nextSeries()` |
| [Panel](./modules/panel.md) | Adds left or right hand side panels | `<ThumbnailList />`, `<MeasurementsTable />` |
| [SOPClassHandler](./modules/sop-class-handler.md) | Determines how retrieved study data is split into "DisplaySets" | `getDisplaySetFromSeries()` |
| [Toolbar](./modules/toolbar.md) | Adds buttons or custom components to the toolbar | Toolbar button, nested buttons, custom |
| [Viewport](./modules/viewport.md) | Adds a component responsible for rendering a "DisplaySet" | `<CornerstoneViewport />`, `<DicomPdfViewport />` |
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
### Contexts
...
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
their functionality. Some example contexts being:
- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST`
- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK`
An extension module can use these to say "Only show this Toolbar Button if the
active viewport is a Cornerstone viewport." This helps us use the appropriate UI
and behaviors depending on the current contexts.
For example, if we have hotkey that "rotates the active viewport", each Viewport
module that supports this behavior can add a command with the same name, scoped
to the appropriate context. When the `command` is fired, the "active contexts"
are used to determine the appropriate implementation of the rotate behavior.
## Consuming Extensions
...
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
project.
```js
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
// prettier-ignore
extensionManager.registerExtensions([ /** **/ ]);
```
The `@ohif/viewer` project handles data fetching, basic routing, wires up UI
services, and is the home to the more bespoke application logic that doesn't
make as much sense to make reusable.
Long-term, replacing the `@ohif/viewer` application and consuming extensions
(and the `ExtensionManager`) in your own project is the ideal path for
applications requiring a high degree of customization that can't be achieved
with current theming, configuration, extension, and services support.
If you're not sure how to achieve your goals with the extensibility available
today, create a GitHub issue!
### `ExtensionManager`
The `ExtensionManager` is a class made available to us via the `@ohif/core`
project (platform/core). Our application instantiates a single instance of it,
and provides a `ServicesManager` and `CommandsManager`.
```js
const commandsManager = new CommandsManager();
const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
```
The `ExtensionManager` only has a few public members:
- `registerExtension` - Registers a single extension
- `registerExtensions` - Registers an array of extensions
- `modules` - An object containing registered extensions by `MODULE_TYPE`
During registration, lifecycle hooks and modules have access to
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
appropriate places in our application.
## Maintained Extensions
@@ -1,12 +1,31 @@
# Lifecylce Hook: preRegistration
If an extension defines the `preRegistration` lifecycle hook, it is called
before any modules are registered to the `ExtensionManager`.
before any modules are registered in the `ExtensionManager`. This hook can be
used to:
- initialize 3rd party libraries
- register event listeners
- add or call services
- add or call commands
The `preRegistration` hook receives an object containing the
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
`configuration` that was provided with the extension at time of registration.
_Example `preRegistration` hook implementation_
```js
export default {
id: 'MyExampleExtension',
/**
* @param {object} params
* @param {object} params.configuration
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns void
*/
preRegistration({ servicesManager, commandsManager, configuration }) {
console.log('Wiring up important stuff.');
+151 -27
View File
@@ -1,34 +1,158 @@
# 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][#]).
- [Overview](#overview)
- [Command Definitions](#command-definitions)
- [Commands Manager](#commands-manager)
- [Instantiating](#instatiating)
- [Public API](#public-api)
- [Contexts](#contexts)
## Overview
An extension can register a Commands Module by defining a `getCommandsModule`
method. The Commands Module allows us to register one or more commands scoped to
specific [contexts](./../index.md#contexts). Commands have several unique
characteristics that make them tremendously powerful:
- Multiple implementations for the same command can be defined
- Only the correct command's implementation will be run, dependent on the
application's "context"
- Commands can be called from extensions, modules, and the consuming application
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'
}
}
export default {
id: 'example-commands-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getCommandsModule({ servicesManager, commandsManager }) {
return {
definitions: {
sayHello: {
commandFn: ({ words }) => {
console.log(words);
},
options: { words: 'Hello!' },
},
},
defaultContext: 'VIEWER',
};
},
};
```
Each definition returned by the Commands Module is registered to the
`ExtensionManager`'s `CommandsManager`.
## Command Definitions
The command definition consists of a named command (`myCommandName` below) and a
`commandFn`. The command name is used to call the command, and the `commandFn`
is the "command" that is actioned.
```js
myCommandName: {
commandFn: ({ viewports, other, options }) => { },
storeContexts: ['viewports'],
options: { words: 'Just kidding! Goodbye!' },
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
}
```
| Property | Type | Description |
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instatiation. |
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
## Command Behavior
**I have many similar commands. How can I share their `commandFn` and make it
reusable?**
This is where `storeContexts` and `options` come in. We use these in our
`setToolActive` command. `storeContexts` helps us identify our `activeViewport`,
and `options` allow us to pass in the name of a tool we would like to set as
active.
**If there are multiple valid commands for the application's active contexts**
- What happens: all commands are run
- When to use: A `clearData` command that cleans up state for multiple
extensions
**If no commands are valid for the application's active contexts**
- What happens: a warning is printed to the console
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
current viewport (PDF or HTML)
## `CommandsManager`
The `CommandsManager` is a class defined in the `@ohif/core` project. A single
instance of it should be defined in the consuming application, and it should be
used when constructing the `ExtensionManager`.
### Instantiating
When we instantiate the `CommandsManager`, we need to pass it two methods:
- `getAppState` - Should return the application's state when called
- `getActiveContexts` - Should return the application's active contexts when
called
These methods are used internally to help determine which commands are currently
valid, and how to provide them with any state they may need at the time they are
called.
```js
const commandsManager = new CommandsManager({
getAppState,
getActiveContexts,
});
```
### Public API
If you would like to run a command in the consuming app or an extension, you can
use one of the following methods:
```js
// Returns all commands for a given context
commandsManager.getContext('string');
// Attempts to run a command
commandsManager.runCommand('speak', { command: 'hello' });
// Run command, but override the active contexts
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']);
```
The `ExtensionManager` handles registering commands and creating contexts, so
most consumer's won't need these methods. If you find yourself using these, ask
yourself "why can't I register these commands via an extension?"
```js
// Used by the `ExtensionManager` to register new commands
commandsManager.registerCommand('context', 'name', commandDefinition);
// Creates a new context; clears the context if it already exists
commandsManager.createContext('string');
```
### Contexts
It is up to the consuming application to define what contexts are possible, and
which ones are currently active. As extensions depend heavily on these, we will
likely publish guidance around creating contexts, and ways to override extension
defined contexts in the near future. If you would like to discuss potential
changes to how contexts work, please don't hesistate to createa new GitHub
issue.
[Some additional information on Contexts can be found here.](./../index.md#contexts)
+55 -1
View File
@@ -1,3 +1,57 @@
# Module: Panel
...
An extension can register a Panel Module by defining a `getPanelModule` method.
The panel module provides the ability to define `menuOptions` and `components`
that can be used by the consuming application. `components` are React Components
that can be displayed in the consuming application's "Panel" Component.
![Panel Extension](../../assets/img/extensions-panel.gif)
<center><i>A panel extension example</i></center>
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
optional provide it's own `context` value.
The `getPanelModule` receives an object containing the `ExtensionManager`'s
associated `ServicesManager` and `CommandsManager`.
```js
import MyComponent from './MyComponent.js';
export default {
id: 'example-panel-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getPanelModule({ servicesManager, commandsManager }) {
return {
menuOptions: [
{
// A suggested icon
// Available icons determined by consuming app
icon: 'list',
// A suggested label
label: 'Magic',
// 'right' or 'left'
from: 'right',
// The target component to toggle open/close
target: 'target-component-id',
// Overrides `defaultContext`, if specified
context: ['ACTIVE_VIEWPORT:MAGIC'],
},
],
components: [
{
id: 'target-component-id',
component: MyComponent,
},
],
defaultContext: ['ROUTE:VIEWER'],
};
},
};
```
@@ -1,3 +1,90 @@
# Module: SOP Class Handler
...
An extension can register a SOP Class Handler Module by defining a
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
own components. It instead defines:
- `sopClassUids`: an array of string SOP Class UIDs that the
`getDisplaySetFromSeries` method should be applied to.
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
display set
A `displaySet` has the following shape:
```js
return {
plugin: 'html',
modality: 'SR',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
```
Where the `plugin` key is used to influence the default `ViewportComponent` for
rendering the `displaySet`. Additional properties are passed to the
`ViewportComponent` and used by the default `StudyBrowser` to render
"thumbnails" for each `displaySet`
## Example SOP Class Handler Module
```js
const SOP_CLASS_UIDS = {
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
};
export default {
id: 'example-sop-class-handler-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getSopClassHandlerModule({ servicesManager, commandsManager }) {
return {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUids: Object.values(SOP_CLASS_UIDS),
/**
* @param {object} series -
* @param {object} study -
* @param {object} dicomWebClient -
* @param {object} authorizationHeaders -
*/
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return {
plugin: 'html',
displaySetInstanceUid: 0,
wadoRoot: study.getData().wadoRoot,
wadoUri: instance.getData().wadouri,
sopInstanceUid: instance.getSOPInstanceUID(),
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(),
authorizationHeaders,
};
},
}
};
```
## `@ohif/viewer` usage
We use the `sopClassHandlerModule`s in three different places:
- `ViewerLocalFileData.js`
- `ViewerRetrieveStudyData.js`
- `StandaloneRouting.js`
Each time, it is used to map study and series data to `displaySets`. It does
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
manager has the method `createDisplaySets` that takes an array of
`sopClassHandlerModules`.
+116 -34
View File
@@ -1,48 +1,130 @@
# 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`.
An extension can register a Toolbar Module by defining a `getToolbarModule`
method. This module is commonly used to define:
![Toolbar Extension](../assets/img/extensions-toolbar.gif)
- [Toolbar buttons](#button-definitions)
- [Nested toolbar menus](#nested-toolbar-menus)
- [Custom components](#custom-components)
<center><i>A toolbar extension example</i></center>
![Toolbar Extension](../../assets/img/extensions-toolbar.gif)
Toolbar components are rendered in the `ToolbarRow` component.
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
For a complete example implementation,
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
## Example Toolbar Module
## 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
The Toolbar Module should return an array of `definitions` and a
`defaultContext`. There are currently a few different variations of definitions,
each one is detailed further down.
```js
definitions: [
...
export default {
id: 'example-toolbar-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getToolbarModule({ servicesManager, commandsManager }) {
return {
definitions: [
/* Array of definitions */
],
defaultContext: ['ROUTE:VIEWER'],
};
},
};
```
## Button Definitions
The simplest definition has the following properties:
```js
{
id: 'StackScroll',
label: 'Stack Scroll',
icon: 'bars',
type: 'setToolActive',
commandName: 'setToolActive',
commandOptions: { toolName: 'StackScroll' },
},
```
| property | description | values |
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
| `id` | Unique string identifier for the definition | \* |
| `label` | User/display friendly to show in UI | \* |
| `icon` | A string name for an icon supported by the consuming application. | \* |
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` |
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` |
| `commandOptions` | (optional) Options to pass the target `commandName` | \* |
| `context` | (optional) Overrides module's `defaultContext` | Array of string context names |
Where a button with a `type` of `setToolActive` has an "active" styling applied
when clicked; removing the active styling from all other buttons.
## Nested Toolbar Menus
You can indicate that buttons should be grouped and nested in a submenu by
including `buttons` property in a definition:
```js
{
id: 'More',
label: 'More',
icon: 'ellipse-circle',
buttons: [
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
...
]
id: 'cstInvert',
label: 'Invert',
icon: 'circle',
type: 'command',
commandName: 'invertViewport',
},
],
},
```
![Toolbar Extension](../../assets/img/extensions-toolbar-nested.gif)
<center><i>Example toolbar button demonstrating nested buttons.</i></center>
## Custom Components
The Toolbar Modules supports rendering custom components in place of the
application's default. In place of the `type`, `commandName`, and
`commandOptions` properties, we instead specify a `CustomComponent`.
```js
{
id: 'Custom',
label: 'Custom',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
}
```
`CustomComponent` components will receive the following props:
The `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
```html
<CustomComponent
parentContext="{parentContext}"
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}"
button="{button}"
key="{button.id}"
activeButtons="{activeButtonsIds}"
isActive="{isActive}"
/>
```
| Property | Type | Description |
| ---------------------- | -------- | ------------------------------- |
| `activeButtons` | string[] | list of active buttons |
| `button` | object | its own definition object |
| `key` | string | React key prop |
| `isActive` | boolean | If current button is active |
| `parentContext` | ? | The parent component's context? |
| `toolbarClickCallback` | func | Callback method for clicks |
+32 -15
View File
@@ -1,19 +1,39 @@
# 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:
An extension can register a Viewport Module by defining a `getViewportModule`
method that returns a React component. Currently, we use viewport components to
add support for:
```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
- 2D Medical Image Viewing (cornerstone ext.)
- Structured Reports as HTML (dicom html ext.)
- Encapsulated PDFs as PDFs (dicom pdf ext.)
- Whole Slide Microscopy Viewing (whole slide ext.)
- etc.
The general pattern is, the [`sopClassHandlerModule`](#) helps us determine
which Viewport Component a set of `sopClassUIDs` should default to. The Viewport
Component receives props containing a display set it should know how to render.
## Viewport Component Props
Each `ViewportComponent` will receive the following props:
```html
<viewportComponent
viewportData="{viewportData}"
viewportIndex="{viewportIndex}"
children="{[children]}"
/>
```
| Property | Type | Description |
| --------------- | --------------- | --------------------------------- |
| `children` | React.element[] | |
| `viewportData` | object | `viewportSpecificData` (probably) |
| `viewportIndex` | number | |
### `@ohif/viewer`
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
@@ -23,7 +43,4 @@ component is used depends on:
![Cornerstone Viewport](../../assets/img/extensions-viewport.png)
<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).
<center><i>An example of three cornerstone Viewports</i></center>
+1 -1
View File
@@ -43,7 +43,7 @@ translate to in practice?
```js
// In the application
const UINotificationService = createUINotificationService();
import UINotificationService from '@ohif/core';
const servicesManager = new ServicesManager();
servicesManager.registerService(UINotificationService);
+8
View File
@@ -1 +1,9 @@
# Viewer
The OHIF Viewing Platform strives to be highly configurable and extensible. This
makes it easier for our community members to keep their "secret sauce" private,
and incentivises contributions back to the platform. The `@ohif/viewer` project
of the platform is the lynchpin that combines everything to create our
application.
- When configuration and themeing aren't enough
+3 -2
View File
@@ -12,8 +12,9 @@ export default {
*/
preRegistration({
servicesManager,
configuration: extensionConfiguration,
servicesManager = {},
commandsManager = {},
configuration = {},
}) {},
/**
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [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
+18
View File
@@ -67,6 +67,24 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
and is connected the redux store. This module is the most prone to change as we
hammer out our Viewport interface.
## Tool Configuration
Tools can be configured through extension configuration using the tools key:
```js
...
cornerstoneExtensionConfig: {
tools: {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
},
},
},
},
...
```
## Resources
### Repositories
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.7.2",
"version": "2.0.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -47,6 +47,7 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.merge": "^4.6.2",
"lodash.throttle": "^4.1.1",
"query-string": "^6.8.3",
"react-cornerstone-viewport": "2.x.x"
@@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component {
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
});
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}
+107 -7
View File
@@ -148,18 +148,118 @@ const commandsModule = ({ servicesManager }) => {
showDownloadViewportModal: ({ title, viewports }) => {
const activeViewportIndex = viewports.activeViewportIndex;
const { UIModalService } = servicesManager.services;
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
if (UIModalService) {
UIModalService.show({
content: CornerstoneViewportDownloadForm,
title,
contentProps: {
activeViewportIndex,
onClose: UIModalService.hide,
},
});
}
},
updateTableWithNewMeasurementData({
toolType,
measurementNumber,
location,
description,
}) {
// Update all measurements by measurement number
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const measurements = measurementApi.tools[toolType].filter(
m => m.measurementNumber === measurementNumber
);
measurements.forEach(measurement => {
measurement.location = location;
measurement.description = description;
measurementApi.updateMeasurement(measurement.toolType, measurement);
});
measurementApi.syncMeasurementsAndToolData();
// Update images in all active viewports
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
},
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
const nearbyTool = {};
let pointNearTool = false;
availableToolTypes.forEach(toolType => {
const elementToolData = cornerstoneTools.getToolState(
element,
toolType
);
if (!elementToolData) {
return;
}
elementToolData.data.forEach((toolData, index) => {
let elementToolInstance = cornerstoneTools.getToolForElement(
element,
toolType
);
if (!elementToolInstance) {
elementToolInstance = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
);
}
if (!elementToolInstance) {
console.warn('Tool not found.');
return undefined;
}
if (
elementToolInstance.pointNearTool(
element,
toolData,
canvasCoordinates
)
) {
pointNearTool = true;
nearbyTool.tool = toolData;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
if (pointNearTool) {
return false;
}
});
return pointNearTool ? nearbyTool : undefined;
},
removeToolState: ({ element, toolType, tool }) => {
cornerstoneTools.removeToolState(element, toolType, tool);
cornerstone.updateImage(element);
},
};
const definitions = {
getNearbyToolData: {
commandFn: actions.getNearbyToolData,
storeContexts: [],
options: {},
},
removeToolState: {
commandFn: actions.removeToolState,
storeContexts: [],
options: {},
},
updateTableWithNewMeasurementData: {
commandFn: actions.updateTableWithNewMeasurementData,
storeContexts: [],
options: {},
},
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: ['viewports'],
+66 -68
View File
@@ -4,6 +4,7 @@ import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
import { SimpleDialog } from '@ohif/ui';
import merge from 'lodash.merge';
function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
@@ -26,30 +27,33 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
/**
*
* @param {object} configuration
* @param {Object} servicesManager
* @param {Object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ servicesManager, configuration = {} }) {
const { UIDialogService } = servicesManager.services;
export default function init({ servicesManager, configuration }) {
const callInputDialog = (data, event, callback) => {
let dialogId = UIDialogService.create({
content: SimpleDialog.InputDialog,
defaultPosition: {
x: (event && event.currentPoints.canvas.x) || 0,
y: (event && event.currentPoints.canvas.y) || 0,
},
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
const { UIDialogService } = servicesManager.services;
if (UIDialogService) {
let dialogId = UIDialogService.create({
centralize: true,
isDraggable: false,
content: SimpleDialog.InputDialog,
useLastPosition: false,
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
measurementData: data ? { description: data.text } : {},
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
},
},
},
});
});
}
};
const { csToolsConfig } = configuration;
@@ -73,61 +77,55 @@ export default function init({ servicesManager, configuration = {} }) {
initCornerstoneTools(defaultCsToolsConfig);
// ~~ Toooools 🙌
const {
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
// Annotations
EraserTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
// Segmentation
BrushTool,
} = csTools;
const tools = [
PanTool,
ZoomTool,
WwwcTool,
MagnifyTool,
StackScrollTool,
StackScrollMouseWheelTool,
csTools.PanTool,
csTools.ZoomTool,
csTools.WwwcTool,
csTools.MagnifyTool,
csTools.StackScrollTool,
csTools.StackScrollMouseWheelTool,
// Touch
PanMultiTouchTool,
ZoomTouchPinchTool,
csTools.PanMultiTouchTool,
csTools.ZoomTouchPinchTool,
// Annotations
EraserTool,
BidirectionalTool,
LengthTool,
AngleTool,
FreehandRoiTool,
EllipticalRoiTool,
DragProbeTool,
RectangleRoiTool,
csTools.ArrowAnnotateTool,
csTools.EraserTool,
csTools.BidirectionalTool,
csTools.LengthTool,
csTools.AngleTool,
csTools.FreehandRoiTool,
csTools.EllipticalRoiTool,
csTools.DragProbeTool,
csTools.RectangleRoiTool,
// Segmentation
BrushTool,
csTools.BrushTool,
];
tools.forEach(tool => csTools.addTool(tool));
csTools.addTool(csTools.ArrowAnnotateTool, {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
/* Add extension tools configuration here. */
const extensionToolsConfiguration = {
ArrowAnnotate: {
configuration: {
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
},
},
});
};
const isEmpty = obj => Object.keys(obj).length < 1;
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) {
/* Add tools with its custom props through extension configuration. */
tools.forEach(tool => {
const toolName = tool.name.replace('Tool', '');
const configurationToolProps = configuration.tools[toolName] || {};
const extensionToolProps = extensionToolsConfiguration[toolName];
let props = merge(extensionToolProps, configurationToolProps);
csTools.addTool(tool, props);
});
} else {
tools.forEach(tool => csTools.addTool(tool));
}
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/extension-vtk
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.0...@ohif/extension-vtk@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/extension-vtk
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.6...@ohif/extension-vtk@1.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [0.54.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.5...@ohif/extension-vtk@0.54.6) (2019-12-07)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.54.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.4...@ohif/extension-vtk@0.54.5) (2019-12-07)
**Note:** Version bump only for package @ohif/extension-vtk
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.54.5",
"version": "1.0.2",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -52,8 +52,8 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.13.3",
"@ohif/ui": "^0.65.3",
"@ohif/core": "^2.0.2",
"@ohif/ui": "^1.0.1",
"cornerstone-tools": "^4.8.0",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+7 -4
View File
@@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component {
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
});
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/core
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.0...@ohif/core@2.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/core
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.13.3",
"version": "2.0.2",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
+1 -1
View File
@@ -59,7 +59,7 @@ export class CommandsManager {
*
* @method
* @param {string} contextName - Namespace for commands
* @returs {Object} - the matched context
* @returns {Object} - the matched context
*/
getContext(contextName) {
const context = this.contexts[contextName];
@@ -26,7 +26,7 @@ export default class ExtensionManager {
const hasConfiguration = Array.isArray(extension);
if (hasConfiguration) {
const [ohifExtension, configuration] = extensions;
const [ohifExtension, configuration] = extension;
this.registerExtension(ohifExtension, configuration);
} else {
this.registerExtension(extension);
@@ -37,6 +37,24 @@ describe('ExtensionManager.js', () => {
// Assert
expect(extensionManager.registerExtension.mock.calls.length).toBe(3);
});
it('calls registerExtension() for each extension passing its configuration if tuple', () => {
const fakeConfiguration = { testing: true };
extensionManager.registerExtension = jest.fn();
// SUT
const fakeExtensions = [
{ one: '1' },
[{ two: '2' }, fakeConfiguration],
{ three: '3 ' },
];
extensionManager.registerExtensions(fakeExtensions);
// Assert
expect(extensionManager.registerExtension.mock.calls[1]).toContain(
fakeConfiguration
);
});
});
describe('registerExtension()', () => {
+15 -9
View File
@@ -20,9 +20,11 @@ import user from './user.js';
import utils from './utils/';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
} from './services';
const OHIF = {
@@ -50,9 +52,11 @@ const OHIF = {
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export {
@@ -79,9 +83,11 @@ export {
measurements,
hangingProtocols,
//
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
};
export { OHIF };
+5 -3
View File
@@ -10,9 +10,11 @@ describe('Top level exports', () => {
'HotkeysManager',
'ServicesManager',
//
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
'UINotificationService',
'UIModalService',
'UIDialogService',
'UIContextMenuService',
'UILabellingFlowService',
//
'utils',
'studies',
+26 -11
View File
@@ -7,10 +7,12 @@ export default class ServicesManager {
}
/**
* Registers a new service.
*
* @param {Object} service
* @param {Object} configuration
*/
registerService(service) {
registerService(service, configuration = {}) {
if (!service) {
log.warn(
'Attempting to register a null/undefined service. Exiting early.'
@@ -18,32 +20,45 @@ export default class ServicesManager {
return;
}
let serviceName = service.name;
if (!serviceName) {
if (!service.name) {
log.warn(`Service name not set. Exiting early.`);
return;
}
if (this.registeredServiceNames.includes(serviceName)) {
if (this.registeredServiceNames.includes(service.name)) {
log.warn(
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
`Service name ${service.name} has already been registered. Exiting before duplicating services.`
);
return;
}
this.services[service.name] = service;
if (service.create) {
this.services[service.name] = service.create({ configuration });
} else {
log.warn(`Service create factory function not defined. Exiting early.`);
return;
}
// Track service registration
this.registeredServiceNames.push(serviceName);
/* Track service registration */
this.registeredServiceNames.push(service.name);
}
/**
* An array of services.
* An array of services, or an array of arrays that contains service
* configuration pairs.
*
* @param {Object[]} services - Array of services
*/
registerServices(services) {
services.forEach(service => this.registerService(service));
services.forEach(service => {
const hasConfiguration = Array.isArray(service);
if (hasConfiguration) {
const [ohifService, configuration] = service;
this.registerService(ohifService, configuration);
} else {
this.registerService(service);
}
});
}
}
@@ -16,18 +16,32 @@ describe('ServicesManager.js', () => {
it('calls registerService() for each service', () => {
servicesManager.registerService = jest.fn();
const fakeServices = [
{ name: 'UINotificationTestService', hide: jest.fn() },
{ name: 'UIModalTestService', hide: jest.fn() },
];
servicesManager.registerServices(fakeServices);
servicesManager.registerServices([
{ name: 'UINotificationTestService', create: jest.fn() },
{ name: 'UIModalTestService', create: jest.fn() },
]);
expect(servicesManager.registerService.mock.calls.length).toBe(2);
});
it('calls registerService() for each service passing its configuration if tuple', () => {
servicesManager.registerService = jest.fn();
const fakeConfiguration = { testing: true };
servicesManager.registerServices([
{ name: 'UINotificationTestService', create: jest.fn() },
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
]);
expect(servicesManager.registerService.mock.calls[1]).toContain(
fakeConfiguration
);
});
});
describe('registerService()', () => {
const fakeService = { name: 'UINotificationService', create: jest.fn() };
it('logs a warning if the service is null or undefined', () => {
const undefinedService = undefined;
const nullService = null;
@@ -39,8 +53,8 @@ describe('ServicesManager.js', () => {
});
it('logs a warning if the service does not have a name', () => {
const serviceWithEmptyName = { name: '', hide: jest.fn() };
const serviceWithoutName = { hide: jest.fn() };
const serviceWithEmptyName = { name: '', create: jest.fn() };
const serviceWithoutName = { create: jest.fn() };
servicesManager.registerService(serviceWithEmptyName);
servicesManager.registerService(serviceWithoutName);
@@ -48,23 +62,37 @@ describe('ServicesManager.js', () => {
expect(log.warn.mock.calls.length).toBe(2);
});
it('tracks which services have been registered', () => {
const service = {
name: 'UINotificationService',
};
it('logs a warning if the service does not have a create factory function', () => {
const serviceWithoutCreate = { name: 'UINotificationService' };
servicesManager.registerService(service);
expect(servicesManager.registeredServiceNames).toContain(service.name);
});
it('logs a warning if the service has an name that has already been registered', () => {
const service = { name: 'UINotificationService' };
servicesManager.registerService(service);
servicesManager.registerService(service);
servicesManager.registerService(serviceWithoutCreate);
expect(log.warn.mock.calls.length).toBe(1);
});
it('tracks which services have been registered', () => {
servicesManager.registerService(fakeService);
expect(servicesManager.registeredServiceNames).toContain(
fakeService.name
);
});
it('logs a warning if the service has an name that has already been registered', () => {
servicesManager.registerService(fakeService);
servicesManager.registerService(fakeService);
expect(log.warn.mock.calls.length).toBe(1);
});
it('pass configuration to service create factory function', () => {
const configuration = { config: 'Some configuration' };
servicesManager.registerService(fakeService, configuration);
expect(fakeService.create.mock.calls[0][0]).toEqual({
configuration,
});
});
});
});
@@ -0,0 +1,66 @@
/**
* UI Context Menu
*
* @typedef {Object} ContextMenuProps
* @property {Event} event The event with tool information.
*/
const name = 'UIContextMenuService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const serviceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
/**
* Show a new UI ContextMenu dialog;
*
* @param {ContextMenuProps} props { event }
*/
function _show({ event }) {
return serviceImplementation._show({
event,
});
}
/**
* Hide a UI ContextMenu dialog;
*
*/
function _hide() {
return serviceImplementation._hide();
}
/**
*
*
* @param {*} {
* show: showImplementation,
* hide: hideImplementation,
* }
*/
function setServiceImplementation({
show: showImplementation,
hide: hideImplementation,
}) {
if (showImplementation) {
serviceImplementation._show = showImplementation;
}
if (hideImplementation) {
serviceImplementation._hide = hideImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -17,59 +17,60 @@
* @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} [centralize=false] Center the dialog on the screen.
* @property {boolean} [preservePosition=true] Use last position instead of default.
* @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.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
const name = 'UIDialogService';
const publicAPI = {
name,
dismiss: _dismiss,
dismissAll: _dismissAll,
create: _create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
const serviceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition }
*/
function create({
function _create({
id,
content,
contentProps,
onStart,
onDrag,
onStop,
centralize = false,
preservePosition = true,
isDraggable = true,
showOverlay = false,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
return serviceImplementation._create({
id,
content,
contentProps,
onStart,
onDrag,
onStop,
centralize,
preservePosition,
isDraggable,
showOverlay,
defaultPosition,
position,
});
}
@@ -78,8 +79,8 @@ function create({
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
function _dismissAll() {
return serviceImplementation._dismissAll();
}
/**
@@ -87,8 +88,8 @@ function dismissAll() {
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
function _dismiss({ id }) {
return serviceImplementation._dismiss({ id });
}
/**
@@ -106,14 +107,19 @@ function setServiceImplementation({
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
serviceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
serviceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
serviceImplementation._create = createImplementation;
}
}
export default createUIDialogService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -0,0 +1,71 @@
/**
* UI Labelling Flow
*
* @typedef {Object} LabellingFlowProps
* @property {Object} defaultPosition The position of the labelling dialog.
* @property {boolean} centralize conditional to center the labelling dialog.
* @property {Object} props The labelling props.
*
*/
const name = 'UILabellingFlowService';
const publicAPI = {
name,
show: _show,
hide: _hide,
setServiceImplementation,
};
const serviceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
};
/**
* Hide a UI LabellingFlow dialog;
*
*/
function _hide() {
return serviceImplementation._hide();
}
/**
* Show a new UI LabellingFlow dialog;
*
* @param {LabellingFlowProps} props { defaultPosition, centralize, props }
*/
function _show({ defaultPosition, centralize, props }) {
return serviceImplementation._show({
defaultPosition,
centralize,
props,
});
}
/**
*
*
* @param {*} {
* show: showImplementation,
* hide: hideImplementation,
* }
*/
function setServiceImplementation({
show: showImplementation,
hide: hideImplementation,
}) {
if (showImplementation) {
serviceImplementation._show = showImplementation;
}
if (hideImplementation) {
serviceImplementation._hide = hideImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -11,28 +11,26 @@
* @property {string} [customClassName=null] The custom class to style the modal.
*/
const uiModalServicePublicAPI = {
name: 'UIModalService',
hide,
show,
const name = 'UIModalService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiModalServiceImplementation = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUIModalService() {
return uiModalServicePublicAPI;
}
/**
* Show a new UI modal;
*
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show({
function _show({
content = null,
contentProps = null,
shouldCloseOnEsc = false,
@@ -41,7 +39,7 @@ function show({
title = null,
customClassName = null,
}) {
return uiModalServiceImplementation._show({
return serviceImplementation._show({
content,
contentProps,
shouldCloseOnEsc,
@@ -57,8 +55,8 @@ function show({
*
* @returns void
*/
function hide() {
return uiModalServiceImplementation._hide();
function _hide() {
return serviceImplementation._hide();
}
/**
@@ -74,11 +72,16 @@ function setServiceImplementation({
show: showImplementation,
}) {
if (hideImplementation) {
uiModalServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiModalServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
}
export default createUIModalService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -10,22 +10,20 @@
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicAPI = {
name: 'UINotificationService',
hide,
show,
const name = 'UINotificationService';
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
const uiNotificationServiceImplementation = {
const serviceImplementation = {
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUINotificationService() {
return uiNotificationServicePublicAPI;
}
/**
* Create and show a new UI notification; returns the
* ID of the created notification.
@@ -33,7 +31,7 @@ function createUINotificationService() {
* @param {Notification} notification { title, message, duration, position, type, autoClose}
* @returns {number} id
*/
function show({
function _show({
title,
message,
duration = 5000,
@@ -41,7 +39,7 @@ function show({
type = 'info',
autoClose = true,
}) {
return uiNotificationServiceImplementation._show({
return serviceImplementation._show({
title,
message,
duration,
@@ -57,8 +55,8 @@ function show({
* @param {number} id - id of the notification to hide/dismiss
* @returns undefined
*/
function hide(id) {
return uiNotificationServiceImplementation._hide({ id });
function _hide(id) {
return serviceImplementation._hide({ id });
}
/**
@@ -74,11 +72,16 @@ function setServiceImplementation({
show: showImplementation,
}) {
if (hideImplementation) {
uiNotificationServiceImplementation._hide = hideImplementation;
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
uiNotificationServiceImplementation._show = showImplementation;
serviceImplementation._show = showImplementation;
}
}
export default createUINotificationService;
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
+10 -6
View File
@@ -1,11 +1,15 @@
import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
import UINotificationService from './UINotificationService';
import UIModalService from './UIModalService';
import UIDialogService from './UIDialogService';
import UIContextMenuService from './UIContextMenuService';
import UILabellingFlowService from './UILabellingFlowService';
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
ServicesManager,
};
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.0.0...@ohif/ui@1.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/ui
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.4...@ohif/ui@1.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [0.65.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.3...@ohif/ui@0.65.4) (2019-12-07)
**Note:** Version bump only for package @ohif/ui
## [0.65.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.2...@ohif/ui@0.65.3) (2019-12-07)
**Note:** Version bump only for package @ohif/ui
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.65.3",
"version": "1.0.1",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -6,9 +6,7 @@
position: relative
.simpleDialog
position: fixed;
top: 0px;
left: 0px;
position: relative;
z-index: 1000;
border: 0;
border-radius: 6px;
@@ -0,0 +1,147 @@
import React, {
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDialog } from '@ohif/ui';
import { useLabellingFlow } from '@ohif/ui';
const ContextMenuContext = createContext(null);
const { Provider } = ContextMenuContext;
export const useContextMenu = () => useContext(ContextMenuContext);
const ContextMenuProvider = ({
children,
service,
contextMenuComponent: ContextMenuComponent,
onDelete,
}) => {
const { create, dismiss } = useDialog();
const { show: showLabellingFlow } = useLabellingFlow();
/**
* Sets the implementation of a context menu service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({
show,
hide,
});
}
}, [hide, service, show]);
const hide = useCallback(() => dismiss({ id: 'context-menu' }), [dismiss]);
/**
* Show the context menu and override its configuration props.
*
* @param {ContextMenuProps} props { eventData, isTouchEvent, onClose, visible }
* @returns void
*/
const show = useCallback(
({ event }) => {
hide();
create({
id: 'context-menu',
isDraggable: false,
preservePosition: false,
content: ContextMenuComponent,
contentProps: {
eventData: event,
onDelete: (nearbyToolData, eventData) =>
onDelete(nearbyToolData, eventData),
onClose: () => dismiss({ id: 'context-menu' }),
onSetLabel: (eventData, measurementData) =>
showLabellingFlow({
event: eventData,
centralize: true,
props: {
measurementData,
skipAddLabelButton: true,
editLocation: true,
},
}),
onSetDescription: (eventData, measurementData) =>
showLabellingFlow({
event: eventData,
centralize: false,
defaultPosition: _getDefaultPosition(eventData),
props: {
measurementData,
editDescriptionOnDialog: true,
},
}),
},
defaultPosition: _getDefaultPosition(event),
});
},
[ContextMenuComponent, create, dismiss, hide, onDelete, showLabellingFlow]
);
const _getDefaultPosition = event => ({
x: (event && event.currentPoints.client.x) || 0,
y: (event && event.currentPoints.client.y) || 0,
});
return (
<Provider
value={{
show,
hide,
}}
>
{children}
</Provider>
);
};
/**
* Higher Order Component to use the context menu methods through a Class Component.
*
* @returns
*/
export const withContextMenu = Component => {
return function WrappedComponent(props) {
const { show, hide } = useContextMenu();
return (
<Component
{...props}
modal={{
show,
hide,
}}
/>
);
};
};
ContextMenuProvider.defaultProps = {
service: null,
};
ContextMenuProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
contextMenuComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
onDelete: PropTypes.func.isRequired,
};
export default ContextMenuProvider;
export const ContextMenuConsumer = ContextMenuContext.Consumer;
@@ -20,7 +20,30 @@ export const useDialog = () => useContext(DialogContext);
const DialogProvider = ({ children, service }) => {
const [isDragging, setIsDragging] = useState(false);
const [dialogs, setDialogs] = useState([]);
const [lastDialogId, setLastDialogId] = useState(null);
const [lastDialogPosition, setLastDialogPosition] = useState(null);
const [centerPositions, setCenterPositions] = useState([]);
useEffect(() => {
setCenterPositions(
dialogs.map(dialog => ({
id: dialog.id,
...getCenterPosition(dialog.id),
}))
);
}, [dialogs]);
const getCenterPosition = id => {
const root = document.querySelector('#root');
const centerX = root.offsetLeft + root.offsetWidth / 2;
const centerY = root.offsetTop + root.offsetHeight / 2;
const item = document.querySelector(`#draggableItem-${id}`);
const itemBounds = item.getBoundingClientRect();
return {
x: centerX - itemBounds.width / 2,
y: centerY - itemBounds.height / 2,
};
};
/**
* Sets the implementation of a dialog service that can be used by extensions.
@@ -42,13 +65,16 @@ const DialogProvider = ({ children, service }) => {
* @property {Object} contentProps The dialog content props.
* @property {boolean} isDraggable Controls if dialog content is draggable or not.
* @property {boolean} showOverlay Controls dialog overlay.
* @property {boolean} centralize Center the dialog on the screen.
* @property {boolean} preservePosition Use last position instead of default.
* @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.
* @property {Function} onStop Called when dragging stops.
* @property {Function} onDrag Called while dragging.
*/
useEffect(() => _bringToFront(lastDialogId), [_bringToFront, lastDialogId]);
/**
* Creates a new dialog and return its id.
*
@@ -64,6 +90,7 @@ const DialogProvider = ({ children, service }) => {
}
setDialogs(dialogs => [...dialogs, { ...props, id: dialogId }]);
setLastDialogId(dialogId);
return dialogId;
}, []);
@@ -75,9 +102,11 @@ const DialogProvider = ({ children, service }) => {
* @property {string} props.id The dialog id.
* @returns void
*/
const dismiss = useCallback(({ id }) => {
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
}, []);
const dismiss = useCallback(
({ id }) =>
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id)),
[]
);
/**
* Dismisses all dialogs.
@@ -101,14 +130,14 @@ const DialogProvider = ({ children, service }) => {
* @param {string} id The dialog id.
* @returns void
*/
const _bringToFront = id => {
const _bringToFront = useCallback(id => {
setDialogs(dialogs => {
const topDialog = dialogs.find(dialog => dialog.id === id);
return topDialog
? [...dialogs.filter(dialog => dialog.id !== id), topDialog]
: [];
: dialogs;
});
};
}, []);
const renderDialogs = () =>
dialogs.map(dialog => {
@@ -116,20 +145,27 @@ const DialogProvider = ({ children, service }) => {
id,
content: DialogContent,
contentProps,
position,
defaultPosition,
centralize = false,
preservePosition = true,
isDraggable = true,
onStart,
onStop,
onDrag,
} = dialog;
let position =
(preservePosition && lastDialogPosition) || defaultPosition;
if (centralize) {
position = centerPositions.find(position => position.id === id);
}
return (
<Draggable
key={id}
disabled={!isDraggable}
position={position}
defaultPosition={lastDialogPosition || defaultPosition}
defaultPosition={position}
bounds="parent"
onStart={event => {
const e = event || window.event;
@@ -169,7 +205,11 @@ const DialogProvider = ({ children, service }) => {
>
<div
id={`draggableItem-${id}`}
className={classNames('DraggableItem', isDragging && 'dragging')}
className={classNames(
'DraggableItem',
isDragging && 'dragging',
isDraggable && 'draggable'
)}
style={{ zIndex: '999', position: 'absolute' }}
onClick={() => _bringToFront(id)}
>
@@ -1,8 +1,8 @@
.DraggableItem
.DraggableItem.draggable
div
cursor: grab !important
.DraggableItem.dragging
.DraggableItem.draggable.dragging
div
cursor: grabbing !important
@@ -0,0 +1,136 @@
import React, {
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDialog } from './DialogProvider';
const LabellingFlowContext = createContext(null);
const { Provider } = LabellingFlowContext;
export const useLabellingFlow = () => useContext(LabellingFlowContext);
const LabellingFlowProvider = ({
children,
service,
labellingComponent: LabellingComponent,
onUpdateLabelling,
}) => {
const { create, dismiss } = useDialog();
/**
* Sets the implementation of a labelling flow service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({
show,
hide,
});
}
}, [hide, service, show]);
const hide = useCallback(() => dismiss({ id: 'labelling' }), [dismiss]);
const show = useCallback(
({ centralize, defaultPosition, props }) => {
hide();
create({
id: 'labelling',
centralize,
isDraggable: false,
showOverlay: true,
content: LabellingComponent,
defaultPosition,
contentProps: {
visible: true,
measurementData: props.measurementData,
labellingDoneCallback: () => dismiss({ id: 'labelling' }),
updateLabelling: labellingData =>
_updateLabellingHandler(labellingData, props.measurementData),
...props,
},
});
},
[LabellingComponent, _updateLabellingHandler, create, dismiss, hide]
);
const _updateLabellingHandler = useCallback(
(labellingData, measurementData) => {
const { location, description, response } = labellingData;
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
onUpdateLabelling(labellingData, measurementData);
},
[onUpdateLabelling]
);
return (
<Provider
value={{
show,
hide,
}}
>
{children}
</Provider>
);
};
/**
* Higher Order Component to use the labelling flow methods through a Class Component.
*
* @returns
*/
export const withLabellingFlow = Component => {
return function WrappedComponent(props) {
const { show, hide } = useLabellingFlow();
return (
<Component
{...props}
modal={{
show,
hide,
}}
/>
);
};
};
LabellingFlowProvider.defaultProps = {
service: null,
};
LabellingFlowProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
labellingComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
onUpdateLabelling: PropTypes.func.isRequired,
};
export default LabellingFlowProvider;
export const LabellingFlowConsumer = LabellingFlowContext.Consumer;
+12
View File
@@ -18,3 +18,15 @@ export {
withDialog,
useDialog,
} from './DialogProvider.js';
export {
default as ContextMenuProvider,
withContextMenu,
useContextMenu,
ContextMenuConsumer,
} from './ContextMenuProvider.js';
export {
default as LabellingFlowProvider,
withLabellingFlow,
useLabellingFlow,
LabellingFlowConsumer,
} from './LabellingFlowProvider.js';
+16
View File
@@ -60,6 +60,14 @@ import {
ModalConsumer,
useModal,
withModal,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
} from './contextProviders';
export {
@@ -117,6 +125,14 @@ export {
DialogProvider,
withDialog,
useDialog,
ContextMenuProvider,
ContextMenuConsumer,
useContextMenu,
withContextMenu,
LabellingFlowProvider,
LabellingFlowConsumer,
useLabellingFlow,
withLabellingFlow,
// Hooks
useDebounce,
useMedia,
+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.0.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.1...@ohif/viewer@3.0.2) (2019-12-11)
**Note:** Version bump only for package @ohif/viewer
## [3.0.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.0.0...@ohif/viewer@3.0.1) (2019-12-09)
**Note:** Version bump only for package @ohif/viewer
# [3.0.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.8...@ohif/viewer@3.0.0) (2019-12-09)
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
### BREAKING CHANGES
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
## [2.11.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.7...@ohif/viewer@2.11.8) (2019-12-07)
**Note:** Version bump only for package @ohif/viewer
## [2.11.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.11.6...@ohif/viewer@2.11.7) (2019-12-07)
**Note:** Version bump only for package @ohif/viewer
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "2.11.7",
"version": "3.0.2",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -45,14 +45,14 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^1.13.3",
"@ohif/extension-cornerstone": "^2.0.0",
"@ohif/core": "^2.0.2",
"@ohif/extension-cornerstone": "^2.0.1",
"@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.5",
"@ohif/extension-vtk": "^1.0.2",
"@ohif/i18n": "^0.52.2",
"@ohif/ui": "^0.65.3",
"@ohif/ui": "^1.0.1",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
+2
View File
@@ -1,6 +1,7 @@
window.config = {
// default: '/'
routerBasename: '/',
whiteLabelling: {},
extensions: [],
showStudyList: true,
filterQueryParam: false,
@@ -69,4 +70,5 @@ window.config = {
// ~ Cornerstone Tools
{ commandName: 'setZoomTool', label: 'Zoom', keys: ['z'] },
],
cornerstoneExtensionConfig: { tools: {} },
};
+1
View File
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
extensions: [],
showStudyList: true,
servers: {
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
dicomWeb: [
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
// This is an array, but we'll only use the first entry for now
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
// This is an array, but we'll only use the first entry for now
+4 -2
View File
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
enableGoogleCloudAdapter: true,
servers: {
// This is an array, but we'll only use the first entry for now
@@ -14,7 +15,8 @@ window.config = {
client_id: 'YOURCLIENTID.apps.googleusercontent.com',
redirect_uri: '/callback', // `OHIFStandaloneViewer.js`
response_type: 'id_token token',
scope: 'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
scope:
'email profile openid https://www.googleapis.com/auth/cloudplatformprojects.readonly https://www.googleapis.com/auth/cloud-healthcare', // email profile openid
// ~ OPTIONAL
post_logout_redirect_uri: '/logout-redirect.html',
revoke_uri: 'https://accounts.google.com/o/oauth2/revoke?token=',
@@ -23,4 +25,4 @@ window.config = {
},
],
studyListFunctionsEnabled: true,
}
};
@@ -1,6 +1,7 @@
window.config = {
// default: '/'
routerBasename: '/',
whiteLabelling: {},
// default: ''
showStudyList: true,
servers: {
+1
View File
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/pwa',
whiteLabelling: {},
showStudyList: true,
servers: {
dicomWeb: [
@@ -1,5 +1,6 @@
window.config = {
routerBasename: '/',
whiteLabelling: {},
showStudyList: true,
servers: {
dicomWeb: [
@@ -1,74 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="OHIF Viewer">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
<meta name="msapplication-TileColor" content="#fff">
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="OHIF Viewer">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
<meta name="msapplication-TileColor" content="#fff">
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
<!-- Targets IE11 -->
<script src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
<script type="text/javascript">
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
</script>
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
<link rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
<link rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
<link rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
<title>OHIF Viewer</title>
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
<!-- Targets IE11 -->
<script
src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
<script type="text/javascript">
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
</script>
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
<!-- WEB FONTS -->
<link
href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap"
rel="stylesheet"
/>
<title>OHIF Viewer</title>
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
<!-- WEB FONTS -->
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
<script>
// If configuration file is a JS Object:
window.config.extensions = [SomeExtension];
// If configuration file is a JS Function:
window.config = () => {
return {
extensions: [SomeExtension]
};
};
</script> -->
</head>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
</body>
<div id="root"></div>
</body>
</html>
@@ -1,86 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="OHIF Viewer">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
<meta name="msapplication-TileColor" content="#fff">
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="theme-color" content="#000000" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="OHIF Viewer">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="@ohif/viewer">
<meta name="msapplication-TileColor" content="#fff">
<meta name="msapplication-TileImage" content="<%= PUBLIC_URL %>assets/mstile-144x144.png">
<meta name="msapplication-config" content="<%= PUBLIC_URL %>assets/browserconfig.xml">
<link rel="manifest" href="<%= PUBLIC_URL %>manifest.json">
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
<!-- Targets IE11 -->
<script src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
<script type="text/javascript">
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
</script>
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
<link rel="shortcut icon" href="<%= PUBLIC_URL %>assets/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="<%= PUBLIC_URL %>assets/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="<%= PUBLIC_URL %>assets/favicon-32x32.png">
<link rel="apple-touch-icon" sizes="57x57" href="<%= PUBLIC_URL %>assets/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="<%= PUBLIC_URL %>assets/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="<%= PUBLIC_URL %>assets/apple-touch-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="<%= PUBLIC_URL %>assets/apple-touch-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="<%= PUBLIC_URL %>assets/apple-touch-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="<%= PUBLIC_URL %>assets/apple-touch-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="<%= PUBLIC_URL %>assets/apple-touch-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= PUBLIC_URL %>assets/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="167x167" href="<%= PUBLIC_URL %>assets/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="<%= PUBLIC_URL %>assets/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="<%= PUBLIC_URL %>assets/apple-touch-icon-1024x1024.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-320x460.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x920.png">
<link rel="apple-touch-startup-image"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-640x1096.png">
<link rel="apple-touch-startup-image"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-750x1294.png">
<link rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 3)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1182x2208.png">
<link rel="apple-touch-startup-image"
media="(device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1242x2148.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-748x1024.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 1)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-768x1004.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1496x2048.png">
<link rel="apple-touch-startup-image"
media="(device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)"
href="<%= PUBLIC_URL %>assets/apple-touch-startup-image-1536x2008.png">
<link rel="icon" type="image/png" sizes="228x228" href="<%= PUBLIC_URL %>assets/coast-228x228.png">
<link rel="yandex-tableau-widget" href="<%= PUBLIC_URL %>assets/yandex-browser-manifest.json">
<title>OHIF Viewer</title>
<!-- Built with: https://polyfill.io/v3/url-builder/ -->
<!-- Targets IE11 -->
<script
src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=default%2CObject.values%2CArray.prototype.flat%2CObject.entries%2CSymbol%2CArray.prototype.includes%2CString.prototype.repeat%2CArray.prototype.find"></script>
<script type="text/javascript">
window.PUBLIC_URL = '<%= PUBLIC_URL %>';
</script>
<script type="text/javascript" src="<%= PUBLIC_URL %>app-config.js"></script>
<script type="module" src="<%= PUBLIC_URL %>init-service-worker.js"></script>
<!-- WEB FONTS -->
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
<title>OHIF Viewer</title>
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
<!-- WEB FONTS -->
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap" rel="stylesheet" />
<!-- EXTENSIONS -->
<!-- <script type="text/javascript" src="path/to/some-extension.js"></script>
<script>
// If configuration file is a JS Object:
window.config.extensions = [SomeExtension];
// If configuration file is a JS Function:
window.config = () => {
return {
extensions: [SomeExtension]
};
};
</script> -->
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
</head>
<div id="root"></div>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<script>
// Note: This is a client access token so there is no issue with having it publicly available
var _rollbarConfig = {
accessToken: "739c64cd39e84592ba062bc29aac9769",
captureUncaught: true,
captureUnhandledRejections: true,
payload: {
environment: "viewer.ohif.org"
}
};
// Rollbar Snippet
!function (r) { var e = {}; function o(n) { if (e[n]) return e[n].exports; var t = e[n] = { i: n, l: !1, exports: {} }; return r[n].call(t.exports, t, t.exports, o), t.l = !0, t.exports } o.m = r, o.c = e, o.d = function (r, e, n) { o.o(r, e) || Object.defineProperty(r, e, { enumerable: !0, get: n }) }, o.r = function (r) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(r, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(r, "__esModule", { value: !0 }) }, o.t = function (r, e) { if (1 & e && (r = o(r)), 8 & e) return r; if (4 & e && "object" == typeof r && r && r.__esModule) return r; var n = Object.create(null); if (o.r(n), Object.defineProperty(n, "default", { enumerable: !0, value: r }), 2 & e && "string" != typeof r) for (var t in r) o.d(n, t, function (e) { return r[e] }.bind(null, t)); return n }, o.n = function (r) { var e = r && r.__esModule ? function () { return r.default } : function () { return r }; return o.d(e, "a", e), e }, o.o = function (r, e) { return Object.prototype.hasOwnProperty.call(r, e) }, o.p = "", o(o.s = 0) }([function (r, e, o) { var n = o(1), t = o(4); _rollbarConfig = _rollbarConfig || {}, _rollbarConfig.rollbarJsUrl = _rollbarConfig.rollbarJsUrl || "https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/2.7.1/rollbar.min.js", _rollbarConfig.async = void 0 === _rollbarConfig.async || _rollbarConfig.async; var a = n.setupShim(window, _rollbarConfig), l = t(_rollbarConfig); window.rollbar = n.Rollbar, a.loadFull(window, document, !_rollbarConfig.async, _rollbarConfig, l) }, function (r, e, o) { var n = o(2); function t(r) { return function () { try { return r.apply(this, arguments) } catch (r) { try { console.error("[Rollbar]: Internal error", r) } catch (r) { } } } } var a = 0; function l(r, e) { this.options = r, this._rollbarOldOnError = null; var o = a++; this.shimId = function () { return o }, "undefined" != typeof window && window._rollbarShims && (window._rollbarShims[o] = { handler: e, messages: [] }) } var i = o(3), d = function (r, e) { return new l(r, e) }, c = function (r) { return new i(d, r) }; function s(r) { return t(function () { var e = Array.prototype.slice.call(arguments, 0), o = { shim: this, method: r, args: e, ts: new Date }; window._rollbarShims[this.shimId()].messages.push(o) }) } l.prototype.loadFull = function (r, e, o, n, a) { var l = !1, i = e.createElement("script"), d = e.getElementsByTagName("script")[0], c = d.parentNode; i.crossOrigin = "", i.src = n.rollbarJsUrl, o || (i.async = !0), i.onload = i.onreadystatechange = t(function () { if (!(l || this.readyState && "loaded" !== this.readyState && "complete" !== this.readyState)) { i.onload = i.onreadystatechange = null; try { c.removeChild(i) } catch (r) { } l = !0, function () { var e; if (void 0 === r._rollbarDidLoad) { e = new Error("rollbar.js did not load"); for (var o, n, t, l, i = 0; o = r._rollbarShims[i++];)for (o = o.messages || []; n = o.shift();)for (t = n.args || [], i = 0; i < t.length; ++i)if ("function" == typeof (l = t[i])) { l(e); break } } "function" == typeof a && a(e) }() } }), c.insertBefore(i, d) }, l.prototype.wrap = function (r, e, o) { try { var n; if (n = "function" == typeof e ? e : function () { return e || {} }, "function" != typeof r) return r; if (r._isWrap) return r; if (!r._rollbar_wrapped && (r._rollbar_wrapped = function () { o && "function" == typeof o && o.apply(this, arguments); try { return r.apply(this, arguments) } catch (o) { var e = o; throw e && ("string" == typeof e && (e = new String(e)), e._rollbarContext = n() || {}, e._rollbarContext._wrappedSource = r.toString(), window._rollbarWrappedError = e), e } }, r._rollbar_wrapped._isWrap = !0, r.hasOwnProperty)) for (var t in r) r.hasOwnProperty(t) && (r._rollbar_wrapped[t] = r[t]); return r._rollbar_wrapped } catch (e) { return r } }; for (var p = "log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleUnhandledRejection,captureEvent,captureDomContentLoaded,captureLoad".split(","), u = 0; u < p.length; ++u)l.prototype[p[u]] = s(p[u]); r.exports = { setupShim: function (r, e) { if (r) { var o = e.globalAlias || "Rollbar"; if ("object" == typeof r[o]) return r[o]; r._rollbarShims = {}, r._rollbarWrappedError = null; var a = new c(e); return t(function () { e.captureUncaught && (a._rollbarOldOnError = r.onerror, n.captureUncaughtExceptions(r, a, !0), n.wrapGlobals(r, a, !0)), e.captureUnhandledRejections && n.captureUnhandledRejections(r, a, !0); var t = e.autoInstrument; return !1 !== e.enabled && (void 0 === t || !0 === t || "object" == typeof t && t.network) && r.addEventListener && (r.addEventListener("load", a.captureLoad.bind(a)), r.addEventListener("DOMContentLoaded", a.captureDomContentLoaded.bind(a))), r[o] = a, a })() } }, Rollbar: c } }, function (r, e) { function o(r, e, o) { if (e.hasOwnProperty && e.hasOwnProperty("addEventListener")) { for (var n = e.addEventListener; n._rollbarOldAdd && n.belongsToShim;)n = n._rLine truncated
<div id="root"></div>
<script>
// Note: This is a client access token so there is no issue with having it publicly available
var _rollbarConfig = {
accessToken: "739c64cd39e84592ba062bc29aac9769",
captureUncaught: true,
captureUnhandledRejections: true,
payload: {
environment: "viewer.ohif.org"
}
};
// Rollbar Snippet
!function (r) { var e = {}; function o(n) { if (e[n]) return e[n].exports; var t = e[n] = { i: n, l: !1, exports: {} }; return r[n].call(t.exports, t, t.exports, o), t.l = !0, t.exports } o.m = r, o.c = e, o.d = function (r, e, n) { o.o(r, e) || Object.defineProperty(r, e, { enumerable: !0, get: n }) }, o.r = function (r) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(r, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(r, "__esModule", { value: !0 }) }, o.t = function (r, e) { if (1 & e && (r = o(r)), 8 & e) return r; if (4 & e && "object" == typeof r && r && r.__esModule) return r; var n = Object.create(null); if (o.r(n), Object.defineProperty(n, "default", { enumerable: !0, value: r }), 2 & e && "string" != typeof r) for (var t in r) o.d(n, t, function (e) { return r[e] }.bind(null, t)); return n }, o.n = function (r) { var e = r && r.__esModule ? function () { return r.default } : function () { return r }; return o.d(e, "a", e), e }, o.o = function (r, e) { return Object.prototype.hasOwnProperty.call(r, e) }, o.p = "", o(o.s = 0) }([function (r, e, o) { var n = o(1), t = o(4); _rollbarConfig = _rollbarConfig || {}, _rollbarConfig.rollbarJsUrl = _rollbarConfig.rollbarJsUrl || "https://cdnjs.cloudflare.com/ajax/libs/rollbar.js/2.7.1/rollbar.min.js", _rollbarConfig.async = void 0 === _rollbarConfig.async || _rollbarConfig.async; var a = n.setupShim(window, _rollbarConfig), l = t(_rollbarConfig); window.rollbar = n.Rollbar, a.loadFull(window, document, !_rollbarConfig.async, _rollbarConfig, l) }, function (r, e, o) { var n = o(2); function t(r) { return function () { try { return r.apply(this, arguments) } catch (r) { try { console.error("[Rollbar]: Internal error", r) } catch (r) { } } } } var a = 0; function l(r, e) { this.options = r, this._rollbarOldOnError = null; var o = a++; this.shimId = function () { return o }, "undefined" != typeof window && window._rollbarShims && (window._rollbarShims[o] = { handler: e, messages: [] }) } var i = o(3), d = function (r, e) { return new l(r, e) }, c = function (r) { return new i(d, r) }; function s(r) { return t(function () { var e = Array.prototype.slice.call(arguments, 0), o = { shim: this, method: r, args: e, ts: new Date }; window._rollbarShims[this.shimId()].messages.push(o) }) } l.prototype.loadFull = function (r, e, o, n, a) { var l = !1, i = e.createElement("script"), d = e.getElementsByTagName("script")[0], c = d.parentNode; i.crossOrigin = "", i.src = n.rollbarJsUrl, o || (i.async = !0), i.onload = i.onreadystatechange = t(function () { if (!(l || this.readyState && "loaded" !== this.readyState && "complete" !== this.readyState)) { i.onload = i.onreadystatechange = null; try { c.removeChild(i) } catch (r) { } l = !0, function () { var e; if (void 0 === r._rollbarDidLoad) { e = new Error("rollbar.js did not load"); for (var o, n, t, l, i = 0; o = r._rollbarShims[i++];)for (o = o.messages || []; n = o.shift();)for (t = n.args || [], i = 0; i < t.length; ++i)if ("function" == typeof (l = t[i])) { l(e); break } } "function" == typeof a && a(e) }() } }), c.insertBefore(i, d) }, l.prototype.wrap = function (r, e, o) { try { var n; if (n = "function" == typeof e ? e : function () { return e || {} }, "function" != typeof r) return r; if (r._isWrap) return r; if (!r._rollbar_wrapped && (r._rollbar_wrapped = function () { o && "function" == typeof o && o.apply(this, arguments); try { return r.apply(this, arguments) } catch (o) { var e = o; throw e && ("string" == typeof e && (e = new String(e)), e._rollbarContext = n() || {}, e._rollbarContext._wrappedSource = r.toString(), window._rollbarWrappedError = e), e } }, r._rollbar_wrapped._isWrap = !0, r.hasOwnProperty)) for (var t in r) r.hasOwnProperty(t) && (r._rollbar_wrapped[t] = r[t]); return r._rollbar_wrapped } catch (e) { return r } }; for (var p = "log,debug,info,warn,warning,error,critical,global,configure,handleUncaughtException,handleUnhandledRejection,captureEvent,captureDomContentLoaded,captureLoad".split(","), u = 0; u < p.length; ++u)l.prototype[p[u]] = s(p[u]); r.exports = { setupShim: function (r, e) { if (r) { var o = e.globalAlias || "Rollbar"; if ("object" == typeof r[o]) return r[o]; r._rollbarShims = {}, r._rollbarWrappedError = null; var a = new c(e); return t(function () { e.captureUncaught && (a._rollbarOldOnError = r.onerror, n.captureUncaughtExceptions(r, a, !0), n.wrapGlobals(r, a, !0)), e.captureUnhandledRejections && n.captureUnhandledRejections(r, a, !0); var t = e.autoInstrument; return !1 !== e.enabled && (void 0 === t || !0 === t || "object" == typeof t && t.network) && r.addEventListener && (r.addEventListener("load", a.captureLoad.bind(a)), r.addEventListener("DOMContentLoaded", a.captureDomContentLoaded.bind(a))), r[o] = a, a })() } }, Rollbar: c } }, function (r, e) { function o(r, e, o) { if (e.hasOwnProperty && e.hasOwnProperty("addEventListener")) { for (var n = e.addEventListener; n._rollbarOldAdd && n.belongsToShim;)n = n._rolLine truncated
// End Rollbar Snippet
</script>
</body>
</script>
</body>
</html>
+127 -52
View File
@@ -4,9 +4,13 @@ import { I18nextProvider } from 'react-i18next';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import { hot } from 'react-hot-loader/root';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import ToolContextMenu from './connectedComponents/ToolContextMenu';
import LabellingManager from './components/Labelling/LabellingManager';
import {
SnackbarProvider,
ModalProvider,
@@ -14,16 +18,23 @@ import {
OHIFModal,
} from '@ohif/ui';
import {
LabellingFlowProvider,
ContextMenuProvider,
} from './appCustomProviders';
import {
CommandsManager,
ExtensionManager,
ServicesManager,
HotkeysManager,
createUINotificationService,
createUIModalService,
createUIDialogService,
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
utils,
redux as reduxOHIF
redux as reduxOHIF,
} from '@ohif/core';
import i18n from '@ohif/i18n';
@@ -46,12 +57,12 @@ import OHIFStandaloneViewer from './OHIFStandaloneViewer';
/** Store */
import { getActiveContexts } from './store/layout/selectors.js';
import store from './store';
const { setUserPreferences } = reduxOHIF.actions;
/** Contexts */
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
const { setUserPreferences } = reduxOHIF.actions;
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
@@ -59,11 +70,6 @@ const commandsManagerConfig = {
getActiveContexts: () => getActiveContexts(store.getState()),
};
/** Services */
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
@@ -79,23 +85,25 @@ window.store = store;
class App extends Component {
static propTypes = {
routerBasename: PropTypes.string.isRequired,
servers: PropTypes.object.isRequired,
//
oidc: PropTypes.array,
whiteLabelling: PropTypes.object,
extensions: PropTypes.arrayOf(
config: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({
id: PropTypes.string.isRequired,
})
),
hotkeys: PropTypes.array,
routerBasename: PropTypes.string.isRequired,
oidc: PropTypes.array,
whiteLabelling: PropTypes.object,
extensions: PropTypes.array,
}),
]).isRequired,
defaultExtensions: PropTypes.array,
};
static defaultProps = {
whiteLabelling: {},
oidc: [],
extensions: [],
config: {
whiteLabelling: {},
oidc: [],
extensions: [],
},
defaultExtensions: [],
};
_appConfig;
@@ -104,31 +112,67 @@ class App extends Component {
constructor(props) {
super(props);
this._appConfig = props;
const { config, defaultExtensions } = props;
const { servers, extensions, hotkeys, oidc } = props;
const appDefaultConfig = {
cornerstoneExtensionConfig: {},
extensions: [],
routerBasename: '/',
whiteLabelling: {},
};
this._appConfig = {
...appDefaultConfig,
...(typeof config === 'function' ? config({ servicesManager }) : config),
};
const {
servers,
hotkeys,
cornerstoneExtensionConfig,
extensions,
oidc,
} = this._appConfig;
this.initUserManager(oidc);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(extensions, hotkeys);
_initServices([
UINotificationService,
UIModalService,
UIDialogService,
UIContextMenuService,
UILabellingFlowService,
]);
_initExtensions(
[...defaultExtensions, ...extensions],
cornerstoneExtensionConfig
);
/*
* Must run after extension commands are registered
* if there is no hotkeys from localStorage set up from config.
*/
_initHotkeys(hotkeys);
_initServers(servers);
initWebWorkers();
}
render() {
const { whiteLabelling, routerBasename } = this.props;
const userManager = this._userManager;
const config = {
appConfig: this._appConfig,
};
const { whiteLabelling, routerBasename } = this._appConfig;
const {
UINotificationService,
UIDialogService,
UILabellingFlowService,
UIModalService,
UIContextMenuService,
} = servicesManager.services;
if (userManager) {
if (this._userManager) {
return (
<AppContext.Provider value={config}>
<AppContext.Provider value={{ appConfig: this._appConfig }}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<OidcProvider store={store} userManager={userManager}>
<UserManagerContext.Provider value={userManager}>
<OidcProvider store={store} userManager={this._userManager}>
<UserManagerContext.Provider value={this._userManager}>
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
@@ -137,7 +181,21 @@ class App extends Component {
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
commandsManager={commandsManager}
>
<ContextMenuProvider
service={UIContextMenuService}
contextMenuComponent={ToolContextMenu}
commandsManager={commandsManager}
>
<OHIFStandaloneViewer
userManager={this._userManager}
/>
</ContextMenuProvider>
</LabellingFlowProvider>
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
@@ -152,7 +210,7 @@ class App extends Component {
}
return (
<AppContext.Provider value={config}>
<AppContext.Provider value={{ appConfig: this._appConfig }}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<Router basename={routerBasename}>
@@ -160,7 +218,19 @@ class App extends Component {
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
<LabellingFlowProvider
service={UILabellingFlowService}
labellingComponent={LabellingManager}
commandsManager={commandsManager}
>
<ContextMenuProvider
service={UIContextMenuService}
contextMenuComponent={ToolContextMenu}
commandsManager={commandsManager}
>
<OHIFStandaloneViewer />
</ContextMenuProvider>
</LabellingFlowProvider>
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
@@ -174,10 +244,10 @@ class App extends Component {
initUserManager(oidc) {
if (oidc && !!oidc.length) {
const firstOpenIdClient = this.props.oidc[0];
const firstOpenIdClient = this._appConfig.oidc[0];
const { protocol, host } = window.location;
const { routerBasename } = this.props;
const { routerBasename } = this._appConfig;
const baseUri = `${protocol}//${host}${routerBasename}`;
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
@@ -213,22 +283,22 @@ function _initServices(services) {
/**
* @param
*/
function _initExtensions(extensions, hotkeys) {
const defaultExtensions = [
function _initExtensions(extensions, cornerstoneExtensionConfig) {
const requiredExtensions = [
GenericViewerCommands,
OHIFCornerstoneExtension,
// WARNING: MUST BE REGISTERED _AFTER_ OHIFCORNERSTONEEXTENSION
[OHIFCornerstoneExtension, cornerstoneExtensionConfig],
/* WARNING: MUST BE REGISTERED _AFTER_ OHIFCornerstoneExtension */
MeasurementsPanel,
];
const mergedExtensions = defaultExtensions.concat(extensions);
const mergedExtensions = requiredExtensions.concat(extensions);
extensionManager.registerExtensions(mergedExtensions);
}
function _initHotkeys(hotkeys) {
const { hotkeyDefinitions = {} } = store.getState().preferences || {};
let updateStore = false;
let hotkeysToUse = hotkeyDefinitions;
// Must run after extension commands are registered
// if there is no hotkeys from localStorate set up from config
if (!Object.keys(hotkeyDefinitions).length) {
hotkeysToUse = hotkeys;
updateStore = true;
@@ -236,7 +306,8 @@ function _initExtensions(extensions, hotkeys) {
if (hotkeysToUse) {
hotkeysManager.setHotkeys(hotkeysToUse);
// set default based on app config
/* Set hotkeys default based on app config. */
hotkeysManager.setDefaultHotKeys(hotkeys);
if (updateStore) {
@@ -264,7 +335,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
return url;
}
// Make sure base_url and url are not duplicating slashes
/*
* Make sure base_url and url are not duplicating slashes.
*/
if (base_url[base_url.length - 1] === '/') {
base_url = base_url.slice(0, base_url.length - 1);
}
@@ -272,7 +345,9 @@ function _makeAbsoluteIfNecessary(url, base_url) {
return base_url + url;
}
// Only wrap/use hot if in dev
/*
* Only wrap/use hot if in dev.
*/
const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App;
export default ExportedApp;
@@ -0,0 +1,51 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ContextMenuProvider } from '@ohif/ui';
const CustomContextMenuProvider = ({
children,
service,
contextMenuComponent,
commandsManager,
}) => {
const onDeleteHandler = (nearbyToolData, eventData) => {
const element = eventData.element;
commandsManager.runCommand('removeToolState', {
element,
toolType: nearbyToolData.toolType,
tool: nearbyToolData.tool,
});
};
return (
<ContextMenuProvider
service={service}
contextMenuComponent={contextMenuComponent}
onDelete={onDeleteHandler}
>
{children}
</ContextMenuProvider>
);
};
CustomContextMenuProvider.defaultProps = {
service: null,
};
CustomContextMenuProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
contextMenuComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
commandsManager: PropTypes.object.isRequired,
};
export default CustomContextMenuProvider;
@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { LabellingFlowProvider } from '@ohif/ui';
const CustomLabellingFlowProvider = ({
children,
service,
labellingComponent,
commandsManager,
}) => {
const onUpdateLabellingHandler = (labellingData, measurementData) => {
commandsManager.runCommand(
'updateTableWithNewMeasurementData',
measurementData
);
};
return (
<LabellingFlowProvider
service={service}
labellingComponent={labellingComponent}
onUpdateLabelling={onUpdateLabellingHandler}
>
{children}
</LabellingFlowProvider>
);
};
CustomLabellingFlowProvider.defaultProps = {
service: null,
};
CustomLabellingFlowProvider.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
labellingComponent: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
commandsManager: PropTypes.object.isRequired,
};
export default CustomLabellingFlowProvider;
@@ -0,0 +1,6 @@
export {
default as LabellingFlowProvider,
} from './LabellingFlowProvider/LabellingFlowProvider.js';
export {
default as ContextMenuProvider,
} from './ContextMenuProvider/ContextMenuProvider.js';
@@ -4,9 +4,7 @@ import OHIF from '@ohif/core';
import moment from 'moment';
import cornerstone from 'cornerstone-core';
//
import jumpToRowItem from './jumpToRowItem.js';
import getMeasurementLocationCallback from './getMeasurementLocationCallback';
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements;
@@ -155,9 +153,11 @@ const mapStateToProps = state => {
};
};
const mapDispatchToProps = dispatch => {
const mapDispatchToProps = (dispatch, ownProps) => {
return {
dispatchRelabel: (event, measurementData, viewportsState) => {
event.persist();
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0;
@@ -167,31 +167,21 @@ const mapDispatchToProps = dispatch => {
return;
}
const { element } = enabledElements[activeViewportIndex];
const eventData = {
event: {
clientX: event.clientX,
clientY: event.clientY,
},
element,
};
const { toolType, measurementId } = measurementData;
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId;
});
const options = {
skipAddLabelButton: true,
editLocation: true,
};
// Clone the tool not to set empty location initially
const toolForLocation = Object.assign({}, tool, { location: null });
getMeasurementLocationCallback(eventData, toolForLocation, options);
if (ownProps.onRelabel) {
ownProps.onRelabel(toolForLocation);
}
},
dispatchEditDescription: (event, measurementData, viewportsState) => {
event.persist();
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0;
@@ -201,26 +191,14 @@ const mapDispatchToProps = dispatch => {
return;
}
const { element } = enabledElements[activeViewportIndex];
const eventData = {
event: {
clientX: event.clientX,
clientY: event.clientY,
},
element,
};
const { toolType, measurementId } = measurementData;
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId;
});
const options = {
editDescriptionOnDialog: true,
};
getMeasurementLocationCallback(eventData, tool, options);
if (ownProps.onEditDescription) {
ownProps.onEditDescription(tool);
}
},
dispatchJumpToRowItem: (
measurementData,
@@ -1,21 +0,0 @@
const setLabellingFlowDataAction = labellingFlowData => ({
type: 'SET_LABELLING_FLOW_DATA',
labellingFlowData,
});
const resetLabellingAndContextMenuAction = state => ({
type: 'RESET_LABELLING_AND_CONTEXT_MENU',
state,
});
const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({
type: 'SET_TOOL_CONTEXT_MENU_DATA',
viewportIndex,
toolContextMenuData,
});
export {
resetLabellingAndContextMenuAction,
setLabellingFlowDataAction,
setToolContextMenuDataAction,
};
@@ -1,33 +0,0 @@
import cornerstoneTools from 'cornerstone-tools';
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
export default function getMeasurementLocationCallback(
eventData,
tool,
options
) {
const { toolType } = tool;
const { element } = eventData;
const doneCallback = updateTableWithNewMeasurementData;
const ToolInstance = cornerstoneTools.getToolForElement(element, toolType);
if (
!ToolInstance ||
!ToolInstance.configuration ||
!ToolInstance.configuration.getMeasurementLocationCallback
) {
console.warn(
'Tool instance configuration is missing: getMeasurementLocationCallback'
);
return;
}
ToolInstance.configuration.getMeasurementLocationCallback(
tool,
eventData,
doneCallback,
options
);
}
@@ -1,3 +1,4 @@
import React from 'react';
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
import init from './init.js';
@@ -7,10 +8,38 @@ export default {
*/
id: 'measurements-table',
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
preRegistration({ servicesManager, commandsManager, configuration = {} }) {
init({ servicesManager, commandsManager, configuration });
},
getPanelModule({ servicesManager }) {
getPanelModule({ servicesManager, commandsManager }) {
const { UILabellingFlowService } = servicesManager.services;
const ExtendedConnectedMeasurementTable = () => (
<ConnectedMeasurementTable
onRelabel={tool => {
if (UILabellingFlowService) {
UILabellingFlowService.show({
centralize: true,
props: {
skipAddLabelButton: true,
editLocation: true,
measurementData: tool,
},
});
}
}}
onEditDescription={tool => {
if (UILabellingFlowService) {
UILabellingFlowService.show({
centralize: true,
props: {
editDescriptionOnDialog: true,
measurementData: tool,
},
});
}
}}
/>
);
return {
menuOptions: [
{
@@ -22,7 +51,7 @@ export default {
components: [
{
id: 'measurement-panel',
component: ConnectedMeasurementTable,
component: ExtendedConnectedMeasurementTable,
},
],
defaultContext: ['VIEWER'],
@@ -1,19 +1,7 @@
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import {
getToolLabellingFlowCallback,
getOnRightClickCallback,
getOnTouchPressCallback,
getResetLabellingAndContextMenu,
} from './labelingFlowCallbacks.js';
import throttle from 'lodash.throttle';
import { SimpleDialog } from '@ohif/ui';
// TODO: This only works because we have a hard dependency on this extension
// We need to decouple and make stuff like this possible w/o bundling this at
// build time
import store from './../../store';
const {
onAdded,
@@ -33,92 +21,18 @@ const MEASUREMENT_ACTION_MAP = {
*
*
* @export
* @param {*} configuration
* @param {Object} servicesManager
* @param {Object} configuration
*/
export default function init({ servicesManager, configuration = {} }) {
const { UIDialogService } = servicesManager.services;
const callInputDialog = (data, event, callback) => {
let dialogId = UIDialogService.create({
content: SimpleDialog.InputDialog,
defaultPosition: {
x: (event && event.currentPoints.canvas.x) || 0,
y: (event && event.currentPoints.canvas.y) || 0,
},
showOverlay: true,
contentProps: {
title: 'Enter your annotation',
label: 'New label',
defaultValue: data ? data.text : '',
onClose: () => UIDialogService.dismiss({ id: dialogId }),
onSubmit: value => {
callback(value);
UIDialogService.dismiss({ id: dialogId });
},
},
});
};
// If these tools were already added by a different extension, we want to replace
// them with the same tools that have an alternative configuration. By passing in
// our custom `getMeasurementLocationCallback`, we can...
const toolLabellingFlowCallback = getToolLabellingFlowCallback(store);
// Removes all tools from all enabled elements w/ provided name
// Not commonly used API, so :eyes: for unknown side-effects
csTools.removeTool('Bidirectional');
csTools.removeTool('Length');
csTools.removeTool('Angle');
csTools.removeTool('FreehandRoi');
csTools.removeTool('EllipticalRoi');
csTools.removeTool('CircleRoi');
csTools.removeTool('RectangleRoi');
csTools.removeTool('ArrowAnnotate');
// Re-add each tool w/ our custom configuration
csTools.addTool(csTools.BidirectionalTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.LengthTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.AngleTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.FreehandRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.EllipticalRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.CircleRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.RectangleRoiTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
});
csTools.addTool(csTools.ArrowAnnotateTool, {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
getTextCallback: (callback, eventDetails) =>
callInputDialog(null, eventDetails, callback),
changeTextCallback: (data, eventDetails, callback) =>
callInputDialog(data, eventDetails, callback),
},
});
export default function init({
servicesManager,
commandsManager,
configuration,
}) {
const {
UIContextMenuService,
UILabellingFlowService,
} = servicesManager.services;
// TODO: MEASUREMENT_COMPLETED (not present in initial implementation)
const onMeasurementsChanged = (action, event) => {
@@ -131,15 +45,43 @@ export default function init({ servicesManager, configuration = {} }) {
this,
'labelmapModified'
);
//
const onRightClick = getOnRightClickCallback(store);
const onTouchPress = getOnTouchPressCallback(store);
const onNewImage = getResetLabellingAndContextMenu(store);
const onMouseClick = getResetLabellingAndContextMenu(store);
const onTouchStart = getResetLabellingAndContextMenu(store);
// Because click gives us the native "mouse up", buttons will always be `0`
// Need to fallback to event.which;
const onRightClick = event => {
if (UIContextMenuService) {
UIContextMenuService.show({ event: event.detail });
}
};
const onTouchPress = event => {
if (UIContextMenuService) {
UIContextMenuService.show({
event: event.detail,
props: {
isTouchEvent: true,
},
});
}
};
const onTouchStart = () => resetLabelligAndContextMenu();
const onMouseClick = () => resetLabelligAndContextMenu();
const resetLabelligAndContextMenu = () => {
if (UILabellingFlowService && UIContextMenuService) {
UILabellingFlowService.hide();
UIContextMenuService.hide();
}
};
// TODO: This makes scrolling painfully slow
// const onNewImage = ...
/*
* Because click gives us the native "mouse up", buttons will always be `0`
* Need to fallback to event.which;
*
*/
const handleClick = cornerstoneMouseClickEvent => {
const mouseUpEvent = cornerstoneMouseClickEvent.detail.event;
const isRightClick = mouseUpEvent.which === 3;
@@ -170,10 +112,11 @@ export default function init({ servicesManager, configuration = {} }) {
csTools.EVENTS.LABELMAP_MODIFIED,
onLabelmapModified
);
//
element.addEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.addEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
element.addEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
// TODO: This makes scrolling painfully slow
// element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
}
@@ -197,10 +140,12 @@ export default function init({ servicesManager, configuration = {} }) {
csTools.EVENTS.LABELMAP_MODIFIED,
onLabelmapModified
);
//
element.removeEventListener(csTools.EVENTS.TOUCH_PRESS, onTouchPress);
element.removeEventListener(csTools.EVENTS.MOUSE_CLICK, handleClick);
element.removeEventListener(csTools.EVENTS.TOUCH_START, onTouchStart);
// TODO: This makes scrolling painfully slow
// element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, onNewImage);
}
@@ -1,144 +0,0 @@
import {
resetLabellingAndContextMenuAction,
setToolContextMenuDataAction,
setLabellingFlowDataAction,
} from './actions.js';
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData.js';
const VIEWPORT_INDEX = 0;
function getOnRightClickCallback(store) {
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
store.dispatch(resetLabellingAndContextMenuAction());
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
const getOnCloseCallback = viewportIndex => {
return function onClose() {
const toolContextMenuData = {
visible: false,
};
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
};
return function onRightClick(event) {
const eventData = event.detail;
const viewportIndex = VIEWPORT_INDEX; // parseInt(eventData.element.dataset.viewportIndex, 10);
const toolContextMenuData = {
eventData,
isTouchEvent: false,
onClose: getOnCloseCallback(viewportIndex),
};
// setToolContextMenuData(viewportIndex, toolContextMenuData);
setToolContextMenuData(0, toolContextMenuData);
};
}
function getOnTouchPressCallback(store) {
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
store.dispatch(resetLabellingAndContextMenuAction());
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
const getOnCloseCallback = viewportIndex => {
return function onClose() {
const toolContextMenuData = {
visible: false,
};
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
);
};
};
return function onTouchPress(event) {
const eventData = event.detail;
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10);
const toolContextMenuData = {
eventData,
isTouchEvent: true,
onClose: getOnCloseCallback(viewportIndex),
};
setToolContextMenuData(viewportIndex, toolContextMenuData);
};
}
function getResetLabellingAndContextMenu(store) {
return function resetLabellingAndContextMenu() {
store.dispatch(resetLabellingAndContextMenuAction());
};
}
/**
*
*
* @param {*} store
* @returns
*/
function getToolLabellingFlowCallback(store) {
const setLabellingFlowData = labellingFlowData => {
store.dispatch(setLabellingFlowDataAction(labellingFlowData));
};
return function toolLabellingFlowCallback(
measurementData,
eventData,
doneCallback,
options = {}
) {
const updateLabelling = ({ location, response, description }) => {
// Update the measurement data with the labelling parameters
if (location) {
measurementData.location = location;
}
measurementData.description = description || '';
if (response) {
measurementData.response = response;
}
updateTableWithNewMeasurementData(measurementData);
};
const labellingDoneCallback = () => {
setLabellingFlowData({ visible: false });
};
const labellingFlowData = {
visible: true,
eventData,
measurementData,
skipAddLabelButton: options.skipAddLabelButton,
editLocation: options.editLocation,
editDescription: options.editDescription,
editResponse: options.editResponse,
editDescriptionOnDialog: options.editDescriptionOnDialog,
labellingDoneCallback,
updateLabelling,
};
setLabellingFlowData(labellingFlowData);
};
}
export {
getToolLabellingFlowCallback,
getOnRightClickCallback,
getOnTouchPressCallback,
getResetLabellingAndContextMenu,
};
@@ -1,29 +0,0 @@
import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
export default function updateTableWithNewMeasurementData({
toolType,
measurementNumber,
location,
description,
}) {
// Update all measurements by measurement number
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
const measurements = measurementApi.tools[toolType].filter(
m => m.measurementNumber === measurementNumber
);
measurements.forEach(measurement => {
measurement.location = location;
measurement.description = description;
measurementApi.updateMeasurement(measurement.toolType, measurement);
});
measurementApi.syncMeasurementsAndToolData();
// Update images in all active viewports
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
}
@@ -1,5 +1,5 @@
.editDescriptionDialog {
position: absolute;
position: relative;
z-index: 300;
width: 320px;
transition: all 300ms linear;
@@ -1,24 +1,15 @@
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
import bounding from '../../lib/utils/bounding.js';
import { getDialogStyle } from './../Labelling/labellingPositionUtils.js';
import './EditDescriptionDialog.css';
export default class EditDescriptionDialog extends Component {
static defaultProps = {
componentRef: React.createRef(),
componentStyle: {},
};
static propTypes = {
description: PropTypes.string,
measurementData: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
componentRef: PropTypes.object,
componentStyle: PropTypes.object,
onUpdate: PropTypes.func.isRequired,
};
@@ -28,14 +19,8 @@ export default class EditDescriptionDialog extends Component {
this.state = {
description: props.measurementData.description || '',
};
this.mainElement = React.createRef();
}
componentDidMount = () => {
bounding(this.mainElement);
};
componentDidUpdate(prevProps) {
if (this.props.description !== prevProps.description) {
this.setState({
@@ -45,16 +30,12 @@ export default class EditDescriptionDialog extends Component {
}
render() {
const style = getDialogStyle(this.props.componentStyle);
return (
<SimpleDialog
headerTitle="Edit Description"
onClose={this.onClose}
onConfirm={this.onConfirm}
rootClass="editDescriptionDialog"
componentRef={this.mainElement}
componentStyle={style}
>
<input
value={this.state.description}
@@ -1,20 +1,16 @@
import { Icon, SelectTree } from '@ohif/ui';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import LabellingTransition from './LabellingTransition.js';
import OHIFLabellingData from './OHIFLabellingData.js';
import PropTypes from 'prop-types';
import bounding from '../../lib/utils/bounding.js';
import cloneDeep from 'lodash.clonedeep';
import { getAddLabelButtonStyle } from './labellingPositionUtils.js';
export default class LabellingFlow extends Component {
static propTypes = {
eventData: PropTypes.object.isRequired,
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
initialTopDistance: PropTypes.number,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
@@ -26,11 +22,6 @@ export default class LabellingFlow extends Component {
const { location, locationLabel, description } = props.measurementData;
let style = props.componentStyle;
if (!props.skipAddLabelButton) {
style = getAddLabelButtonStyle(props.measurementData, props.eventData);
}
this.state = {
location,
locationLabel,
@@ -38,20 +29,17 @@ export default class LabellingFlow extends Component {
skipAddLabelButton: props.skipAddLabelButton,
editDescription: props.editDescription,
editLocation: props.editLocation,
componentStyle: style,
confirmationState: false,
displayComponent: true,
};
this.mainElement = React.createRef();
this.descriptionInput = React.createRef();
this.initialItems = OHIFLabellingData;
this.currentItems = cloneDeep(this.initialItems);
}
componentDidUpdate = () => {
this.repositionComponent();
if (this.state.editDescription) {
this.descriptionInput.current.focus();
}
@@ -63,36 +51,14 @@ export default class LabellingFlow extends Component {
mainElementClassName += ' editDescription';
}
const style = Object.assign({}, this.state.componentStyle);
if (this.state.skipAddLabelButton) {
if (style.left - 160 < 0) {
style.left = 0;
} else {
style.left -= 160;
}
}
if (this.state.editLocation) {
style.maxHeight = '70vh';
if (!this.initialTopDistance) {
this.initialTopDistance = window.innerHeight - window.innerHeight * 0.3;
style.top = `${this.state.componentStyle.top -
this.initialTopDistance / 2}px`;
} else {
style.top = `${this.state.componentStyle.top}px`;
}
}
return (
<LabellingTransition
displayComponent={this.state.displayComponent}
onTransitionExit={this.props.labellingDoneCallback}
>
<>
<div className="labellingComponent-overlay"></div>
<div
className={mainElementClassName}
style={style}
ref={this.mainElement}
onMouseLeave={this.fadeOutAndLeave}
onMouseEnter={this.clearFadeOutTimer}
@@ -130,9 +96,8 @@ export default class LabellingFlow extends Component {
<SelectTree
items={this.currentItems}
columns={1}
onSelected={this.selectTreeSelectCalback}
onSelected={this.selectTreeSelectCallback}
selectTreeFirstTitle="Assign Label"
onComponentChange={this.repositionComponent}
/>
);
} else {
@@ -195,33 +160,17 @@ export default class LabellingFlow extends Component {
}
};
relabel = event => {
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
const componentStyle = {
top: event.nativeEvent.y - viewportTopPosition - 55,
left: event.nativeEvent.x,
};
this.setState({
editLocation: true,
componentStyle,
});
};
relabel = event => this.setState({ editLocation: true });
setDescriptionUpdateMode = () => {
this.descriptionInput.current.focus();
this.setState({
editDescription: true,
});
this.setState({ editDescription: true });
};
descriptionCancel = () => {
const { description = '' } = cloneDeep(this.state);
this.descriptionInput.current.value = description;
this.setState({
editDescription: false,
});
this.setState({ editDescription: false });
};
handleKeyPress = e => {
@@ -240,22 +189,15 @@ export default class LabellingFlow extends Component {
});
};
selectTreeSelectCalback = (event, itemSelected) => {
selectTreeSelectCallback = (event, itemSelected) => {
const location = itemSelected.value;
this.props.updateLabelling({ location });
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
const componentStyle = {
top: event.nativeEvent.y - viewportTopPosition - 25,
left: event.nativeEvent.x,
};
this.setState({
editLocation: false,
confirmationState: true,
location: itemSelected.value,
locationLabel: itemSelected.label,
componentStyle,
});
if (this.isTouchScreen) {
@@ -276,18 +218,13 @@ export default class LabellingFlow extends Component {
fadeOutAndLeave = () => {
// Wait for 1 sec to dismiss the labelling component
this.fadeOutTimer = setTimeout(() => {
this.setState({
displayComponent: false,
});
}, 1000);
this.fadeOutTimer = setTimeout(
() => this.setState({ displayComponent: false }),
1000
);
};
fadeOutAndLeaveFast = () => {
this.setState({
displayComponent: false,
});
};
fadeOutAndLeaveFast = () => this.setState({ displayComponent: false });
clearFadeOutTimer = () => {
if (!this.fadeOutTimer) {
@@ -296,29 +233,4 @@ export default class LabellingFlow extends Component {
clearTimeout(this.fadeOutTimer);
};
calculateTopDistance = () => {
const height = window.innerHeight - window.innerHeight * 0.3;
let top = this.state.componentStyle.top - height / 2 + 55;
if (top < 0) {
top = 0;
} else {
if (top + height > window.innerHeight) {
top -= top + height - window.innerHeight;
}
}
return top;
};
repositionComponent = () => {
// SetTimeout for the css animation to end.
setTimeout(() => {
bounding(this.mainElement);
if (this.state.editLocation) {
this.mainElement.current.style.maxHeight = '70vh';
const top = this.calculateTopDistance();
this.mainElement.current.style.top = `${top}px`;
}
}, 200);
};
}
@@ -1,18 +1,9 @@
.labellingComponent-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10;
background-color: rgba(0, 0, 0, 0.8);
}
.labellingComponent {
position: absolute;
position: relative;
text-align: center;
z-index: 999;
transition: all 200ms linear;
max-height: 500px;
}
.labellingComponent .selectedLabel,
@@ -87,9 +78,7 @@
border: none;
}
.labellingComponent.editDescription
.locationDescriptionWrapper
#descriptionInput {
.labellingComponent.editDescription .locationDescriptionWrapper #descriptionInput {
visibility: visible;
}
@@ -1,21 +1,16 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cloneDeep from 'lodash.clonedeep';
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
import LabellingFlow from './LabellingFlow.js';
import './LabellingManager.css';
export default class LabellingManager extends Component {
static propTypes = {
eventData: PropTypes.object.isRequired,
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
@@ -41,7 +36,6 @@ export default class LabellingManager extends Component {
}
this.state = {
componentStyle: getComponentPosition(props.eventData),
skipAddLabelButton: props.skipAddLabelButton,
editLocation: editLocation,
editDescription: props.editDescription,
@@ -75,20 +69,13 @@ export default class LabellingManager extends Component {
<EditDescriptionDialog
onCancel={this.props.labellingDoneCallback}
onUpdate={this.descriptionDialogUpdate}
componentRef={this.editDescriptionDialog}
componentStyle={this.state.componentStyle}
measurementData={measurementData}
/>
);
}
if (editLocation || editDescription) {
return (
<LabellingFlow
{...this.props}
componentStyle={this.state.componentStyle}
/>
);
return <LabellingFlow {...this.props} />;
}
};
@@ -98,33 +85,19 @@ export default class LabellingManager extends Component {
if (editDescription) {
measurementData.description = undefined;
}
if (editLocation) {
measurementData.location = undefined;
}
};
responseDialogUpdate = response => {
this.props.updateLabelling({
response,
});
this.props.updateLabelling({ response });
this.props.labellingDoneCallback();
};
descriptionDialogUpdate = description => {
this.props.updateLabelling({
description,
});
this.props.updateLabelling({ description });
this.props.labellingDoneCallback();
};
}
function getComponentPosition(eventData) {
const {
event: { clientX: left, clientY: top },
} = eventData;
return {
left,
top,
};
}
@@ -1,53 +0,0 @@
import cornerstone from 'cornerstone-core';
const buttonSize = {
width: 96,
height: 28,
};
export function getAddLabelButtonStyle(measurementData, eventData) {
const { start, end } = measurementData.handles;
const { client } = eventData.currentPoints;
const clientStart = cornerstone.pixelToCanvas(eventData.element, start);
const clientEnd = cornerstone.pixelToCanvas(eventData.element, end);
const canvasOffSetLeft = client.x - clientStart.x;
const canvasOffSetTop = client.y - clientStart.y;
const position = {
left: clientEnd.x + canvasOffSetLeft,
top: clientEnd.y + canvasOffSetTop,
};
if (start.y > end.y) {
position.top -= buttonSize.height;
}
if (start.x > end.x) {
position.left -= buttonSize.width;
}
return position;
}
export function getDialogStyle(componentStyle) {
const style = Object.assign({}, componentStyle);
const dialogProps = {
width: 320,
height: 230,
};
// Get max values to avoid position out of the screen
const maxLeft = window.innerWidth - dialogProps.width;
const maxTop = window.innerHeight - dialogProps.height;
// Positioning the dialog with its center on the click event
style.left -= dialogProps.width / 2;
style.top -= dialogProps.height / 2;
if (style.left > maxLeft) {
style.left = maxLeft;
}
if (style.top > maxTop) {
style.top = maxTop;
}
return style;
}
@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import LabellingOverlay from './LabellingOverlay';
const mapStateToProps = state => {
if (!state.ui || !state.ui.labelling) {
return {
visible: false,
};
}
const labellingFlowData = state.ui.labelling;
return {
visible: false,
...labellingFlowData,
};
};
const ConnectedLabellingOverlay = connect(
mapStateToProps,
null
)(LabellingOverlay);
export default ConnectedLabellingOverlay;
@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import ToolContextMenu from './ToolContextMenu';
const mapStateToProps = (state, ownProps) => {
if (!state.ui || !state.ui.contextMenu) {
return {
visible: false,
};
}
const { viewportIndex } = ownProps;
const toolContextMenuData = state.ui.contextMenu[viewportIndex];
return {
...toolContextMenuData,
};
};
const ConnectedToolContextMenu = connect(
mapStateToProps,
null
)(ToolContextMenu);
export default ConnectedToolContextMenu;
@@ -35,7 +35,10 @@ const mapDispatchToProps = (dispatch, ownProps) => {
// set new language
i18n.changeLanguage(language);
ownProps.hide();
if (ownProps.hide) {
ownProps.hide();
}
dispatch(
setUserPreferences({
windowLevelData,
@@ -1,23 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import LabellingManager from '../components/Labelling/LabellingManager';
class LabellingOverlay extends Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
};
static defaultProps = {
visible: false,
};
render() {
if (!this.props.visible) {
return null;
}
return <LabellingManager {...this.props} />;
}
}
export default LabellingOverlay;
@@ -1,5 +1,5 @@
.ToolContextMenu {
position: absolute;
position: relative;
background-color: white;
border: 1px solid white;
border-radius: 5px;
@@ -1,9 +1,6 @@
import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
// This whole component should live in the Measurements Extension :thinking:
import getMeasurementLocationCallback from '../appExtensions/MeasurementsPanel/getMeasurementLocationCallback';
import { commandsManager } from './../App.js';
import './ToolContextMenu.css';
@@ -17,232 +14,119 @@ const toolTypes = [
'RectangleRoi',
];
let defaultDropdownItems = [
{
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) => {
const element = eventData.element;
cornerstoneTools.removeToolState(
element,
nearbyToolData.toolType,
nearbyToolData.tool
);
cornerstone.updateImage(element);
const ToolContextMenu = ({
onSetLabel,
onSetDescription,
isTouchEvent,
eventData,
onClose,
onDelete,
}) => {
const defaultDropdownItems = [
{
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) =>
onDelete(nearbyToolData, eventData),
},
},
{
actionType: 'setLabel',
action: ({ nearbyToolData, eventData }) => {
const { tool } = nearbyToolData;
const options = {
skipAddLabelButton: true,
editLocation: true,
};
getMeasurementLocationCallback(eventData, tool, options);
{
actionType: 'setLabel',
action: ({ nearbyToolData, eventData }) => {
const { tool: measurementData } = nearbyToolData;
onSetLabel(eventData, measurementData);
},
},
},
{
actionType: 'setDescription',
action: ({ nearbyToolData, eventData }) => {
const { tool } = nearbyToolData;
const options = {
editDescriptionOnDialog: true,
};
getMeasurementLocationCallback(eventData, tool, options);
{
actionType: 'setDescription',
action: ({ nearbyToolData, eventData }) => {
const { tool: measurementData } = nearbyToolData;
onSetDescription(eventData, measurementData);
},
},
},
];
];
function getNearbyToolData(element, coords, toolTypes) {
const nearbyTool = {};
let pointNearTool = false;
const getDropdownItems = (eventData, isTouchEvent = false) => {
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
element: eventData.element,
canvasCoordinates: eventData.currentPoints.canvas,
availableToolTypes: toolTypes,
});
toolTypes.forEach(toolType => {
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) {
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
if (
isTouchEvent &&
nearbyToolData &&
nearbyToolData.toolType === 'arrowAnnotate'
) {
return;
}
toolData.data.forEach(function(data, index) {
// TODO: Fix this, it's ugly
let toolInterface = cornerstoneTools.getToolForElement(element, toolType);
if (!toolInterface) {
toolInterface = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
);
}
let dropdownItems = [];
if (nearbyToolData) {
defaultDropdownItems.forEach(item => {
item.params = {
eventData,
nearbyToolData,
};
if (!toolInterface) {
throw new Error('Tool not found.');
}
if (toolInterface.pointNearTool(element, data, coords)) {
pointNearTool = true;
nearbyTool.tool = data;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
if (pointNearTool) {
return false;
}
});
return pointNearTool ? nearbyTool : undefined;
}
function getDropdownItems(eventData, isTouchEvent = false) {
const nearbyToolData = getNearbyToolData(
eventData.element,
eventData.currentPoints.canvas,
toolTypes
);
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
if (
isTouchEvent &&
nearbyToolData &&
nearbyToolData.toolType === 'arrowAnnotate'
) {
return;
}
let dropdownItems = [];
if (nearbyToolData) {
defaultDropdownItems.forEach(function(item) {
item.params = {
eventData,
nearbyToolData,
};
if (item.actionType === 'Delete') {
item.text = 'Delete measurement';
}
if (item.actionType === 'setLabel') {
item.text = 'Relabel';
}
if (item.actionType === 'setDescription') {
item.text = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`;
}
dropdownItems.push(item);
});
}
return dropdownItems;
}
class ToolContextMenu extends Component {
static propTypes = {
isTouchEvent: PropTypes.bool.isRequired,
eventData: PropTypes.object,
onClose: PropTypes.func,
visible: PropTypes.bool.isRequired,
};
static defaultProps = {
visible: true,
isTouchEvent: false,
};
constructor(props) {
super(props);
this.mainElement = React.createRef();
}
render() {
if (!this.props.eventData) {
return null;
}
const { isTouchEvent, eventData } = this.props;
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
// Skip if there is no dropdown item
if (!dropdownItems.length) {
return '';
}
const dropdownComponents = dropdownItems.map(item => {
const itemOnClick = event => {
item.action(item.params);
if (this.props.onClose) {
this.props.onClose();
if (item.actionType === 'Delete') {
item.text = 'Delete measurement';
}
};
return (
<li key={item.actionType}>
<button className="form-action" onClick={itemOnClick}>
<span key={item.actionType}>{item.text}</span>
</button>
</li>
);
});
if (item.actionType === 'setLabel') {
item.text = 'Relabel';
}
const position = {
top: `${eventData.currentPoints.canvas.y}px`,
left: `${eventData.currentPoints.canvas.x}px`,
};
if (item.actionType === 'setDescription') {
item.text = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`;
}
return (
<div className="ToolContextMenu" style={position} ref={this.mainElement}>
<ul className="bounded">{dropdownComponents}</ul>
dropdownItems.push(item);
});
}
return dropdownItems;
};
const itemOnClickHandler = (action, params, onClose) => {
action(params);
if (onClose) {
onClose();
}
};
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
return (
dropdownItems.length &&
eventData && (
<div className="ToolContextMenu">
<ul className="bounded">
{dropdownItems.map(({ params, action, text, actionType }) => (
<li key={actionType}>
<button
className="form-action"
onClick={() => itemOnClickHandler(action, params, onClose)}
>
<span key={actionType}>{text}</span>
</button>
</li>
))}
</ul>
</div>
);
}
)
);
};
componentDidMount = () => {
if (this.mainElement.current) {
this.updateElementPosition();
}
};
ToolContextMenu.propTypes = {
isTouchEvent: PropTypes.bool.isRequired,
eventData: PropTypes.object,
onClose: PropTypes.func,
};
componentDidUpdate = () => {
if (this.mainElement.current) {
this.updateElementPosition();
}
};
updateElementPosition = () => {
const {
offsetParent,
offsetTop,
offsetHeight,
offsetWidth,
offsetLeft,
} = this.mainElement.current;
const { eventData } = this.props;
if (offsetTop + offsetHeight > offsetParent.offsetHeight) {
const offBoundPixels =
offsetTop + offsetHeight - offsetParent.offsetHeight;
const top = eventData.currentPoints.canvas.y - offBoundPixels;
this.mainElement.current.style.top = `${top > 0 ? top : 0}px`;
}
if (offsetLeft + offsetWidth > offsetParent.offsetWidth) {
const offBoundPixels =
offsetLeft + offsetWidth - offsetParent.offsetWidth;
const left = eventData.currentPoints.canvas.x - offBoundPixels;
this.mainElement.current.style.left = `${left > 0 ? left : 0}px`;
}
};
}
ToolContextMenu.defaultProps = {
isTouchEvent: false,
};
export default ToolContextMenu;
@@ -7,7 +7,6 @@ import OHIF from '@ohif/core';
import moment from 'moment';
import ConnectedHeader from './ConnectedHeader.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ConnectedViewerMain from './ConnectedViewerMain.js';
import SidePanel from './../components/SidePanel.js';
@@ -317,7 +316,6 @@ class Viewer extends Component {
)}
</SidePanel>
</div>
<ConnectedLabellingOverlay />
</>
);
}
@@ -2,7 +2,6 @@ import './ViewerMain.css';
import { Component } from 'react';
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
import ConnectedToolContextMenu from './ConnectedToolContextMenu.js';
import PropTypes from 'prop-types';
import React from 'react';
@@ -153,7 +152,6 @@ class ViewerMain extends Component {
setViewportData={this.setViewportData}
>
{/* Children to add to each viewport that support children */}
<ConnectedToolContextMenu />
</ConnectedViewportGrid>
)}
</div>
+2 -2
View File
@@ -7,7 +7,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';
function installViewer(props, containerId = 'root', callback) {
function installViewer(config, containerId = 'root', callback) {
const container = document.getElementById(containerId);
if (!container) {
@@ -16,7 +16,7 @@ function installViewer(props, containerId = 'root', callback) {
);
}
return ReactDOM.render(<App {...props} />, container, callback);
return ReactDOM.render(<App config={config} />, container, callback);
}
export { App, installViewer };
+15 -14
View File
@@ -18,35 +18,36 @@ import ReactDOM from 'react-dom';
* "baked in" to the published application.
*
* Depending on your use case/needs, you may want to consider not adding any extensions
* by default HERE, and instead provide them via the configuration specified at
* `window.config.extensions`, or by using the exported `App` component, and passing
* in your extensions as props.
* by default HERE, and instead provide them via the extensions configuration key or
* by using the exported `App` component, and passing in your extensions as props using
* the defaultExtensions property.
*/
import OHIFVTKExtension from '@ohif/extension-vtk';
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
// Default Settings
/*
* Default Settings
*/
let config = {};
const appDefaults = {
routerBasename: '/',
};
if (window) {
config = window.config || {};
config.extensions = [
}
const appProps = {
config,
defaultExtensions: [
OHIFVTKExtension,
OHIFDicomHtmlExtension,
OHIFDicomMicroscopyExtension,
OHIFDicomPDFExtension,
];
}
],
};
const appProps = Object.assign({}, appDefaults, config);
// Create App
/** Create App */
const app = React.createElement(App, appProps, null);
// Render
/** Render */
ReactDOM.render(app, document.getElementById('root'));
+1 -3
View File
@@ -6,18 +6,16 @@ import {
} from 'redux/es/redux.js';
// import { createLogger } from 'redux-logger';
import layoutReducers from './layout/reducers.js';
import { reducer as oidcReducer } from 'redux-oidc';
import { redux } from '@ohif/core';
import thunkMiddleware from 'redux-thunk';
// Combine our @ohif/core, ui, and oidc reducers
// Combine our @ohif/core and oidc reducers
// Set init data, using values found in localStorage
const { reducers, localStorage, sessionStorage } = redux;
const middleware = [thunkMiddleware];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
reducers.ui = layoutReducers;
reducers.oidc = oidcReducer;
const rootReducer = combineReducers(reducers);
@@ -1,33 +0,0 @@
const defaultState = {
labelling: {},
contextMenu: {},
};
const ui = (state = defaultState, action) => {
switch (action.type) {
case 'SET_LABELLING_FLOW_DATA': {
const labelling = Object.assign({}, action.labellingFlowData);
return Object.assign({}, state, { labelling });
}
case 'SET_TOOL_CONTEXT_MENU_DATA': {
const contextMenu = Object.assign({}, state.contextMenu);
contextMenu[action.viewportIndex] = Object.assign(
{},
action.toolContextMenuData
);
return Object.assign({}, state, { contextMenu });
}
case 'RESET_LABELLING_AND_CONTEXT_MENU':
return Object.assign({}, state, {
labelling: defaultState.labelling,
contextMenu: defaultState.contextMenu,
});
default:
return state;
}
};
export default ui;
+2 -21
View File
@@ -2654,16 +2654,6 @@
dependencies:
"@types/node" "^12.11.1"
"@ohif/extension-cornerstone@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@ohif/extension-cornerstone/-/extension-cornerstone-2.0.0.tgz#b4ee3b594212502192cdd311a100354857280548"
integrity sha512-hvJ3t2GRcu906NiYpUWaJIB9ja/M/MKEYLy19t4XBFpqf4VqoezR2KJpZelmZjQscXGaDQeZgvr3DJO5Nva8Uw==
dependencies:
"@babel/runtime" "^7.5.5"
classnames "^2.2.6"
lodash.throttle "^4.1.1"
react-cornerstone-viewport "0.1.30"
"@ohif/i18n@^0.2.3":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@ohif/i18n/-/i18n-0.2.5.tgz#2b92a78109823ce0f50dd590201d980a5fd64974"
@@ -12246,7 +12236,7 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
lodash.merge@^4.4.0, lodash.merge@^4.6.1:
lodash.merge@^4.4.0, lodash.merge@^4.6.1, lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
@@ -13103,7 +13093,7 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment@2.24.0, moment@>=1.6.0, moment@^2.23.0, moment@^2.24.0:
moment@2.24.0, moment@>=1.6.0, moment@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
@@ -15839,15 +15829,6 @@ react-codemirror2@^6.0.0:
resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-6.0.0.tgz#180065df57a64026026cde569a9708fdf7656525"
integrity sha512-D7y9qZ05FbUh9blqECaJMdDwKluQiO3A9xB+fssd5jKM7YAXucRuEOlX32mJQumUvHUkHRHqXIPBjm6g0FW0Ag==
react-cornerstone-viewport@0.1.30:
version "0.1.30"
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-0.1.30.tgz#3546a5164feef5f1f3b45ade10fd9a66f82fd101"
integrity sha512-DGxdpS7FJvWi57NhpKhKMdaW3go/Fs/zfWJFtaEzGVHq2zQnEEV5G1pamC4ttRhKMfzy7T7y4h14xUVzE7IqPA==
dependencies:
lodash.debounce "^4.0.8"
moment "^2.23.0"
stylus "^0.54.5"
react-cornerstone-viewport@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-2.1.0.tgz#1f353e3b0a57c45add70e5040b819290d9f55f4b"