feat: Added documentation for OHIF-v3 (#2450)

* Added docs with new screenshots

* Added doc to architecture

* Added documentations to various extension modules

* Added more documentation to modes

* Added docs to managers

* Added docs for services

* Fixed deployment docs

* Added white labelling documentation

* Added i18n docs and measurement export
This commit is contained in:
Alireza 2021-06-15 11:15:29 -04:00 committed by GitHub
parent 1bf651e763
commit 5643f8f6d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 5251 additions and 1523 deletions

2
docs/.gitignore vendored
View File

@ -1 +1 @@
_book/ ./_book/*

View File

@ -8,6 +8,7 @@ _Requirements:_
Make sure you have the [`gitbook-cli`](https://www.npmjs.com/package/gitbook-cli) installed globally: Make sure you have the [`gitbook-cli`](https://www.npmjs.com/package/gitbook-cli) installed globally:
(Note that gitbook 3.2.3 works with node 10.19.0 and node 10.24.0, but not with node 14.x.)
> `npm install -g gitbook-cli` > `npm install -g gitbook-cli`
### Editing and Previewing Changes ### Editing and Previewing Changes

1
docs/latest/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_book/

View File

@ -10,11 +10,10 @@
</div> </div>
</div> </div>
> ATTENTION! You are looking at the docs for the `React` version of the OHIF > ATTENTION! You are looking at the docs for the `OHIF-v3` Viewer (third time is always a charm). If you're looking for the our `OHIF-v2` (React stable version) or OHIF-v1 (deprecated `Meteor` version) select it's version from the dropdown box in the top left corner
> Viewer. If you're looking for the `Meteor` version's documentation (now
> deprecated), select it's version from the dropdown box in the top left corner
> of this page. > of this page.
&nbsp;
# Introduction # Introduction
The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source, The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source,
@ -23,9 +22,45 @@ Archives that support [DicomWeb][dicom-web], and offers support for mapping to
proprietary API formats. OHIF maintained extensions add support for viewing, proprietary API formats. OHIF maintained extensions add support for viewing,
annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes). annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
![OHIF Viewer Screenshot](../assets/img/viewer.png)
<center><i>The <strong>OHIF Viewer</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
![OHIF Viewer Screenshot](./assets/img/OHIF-viewer.png)
<center><i>The <strong>OHIF Viewer v3</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
&nbsp;
## What's new in `OHIF-v3`
`OHIF-v3` is our second try for a React-based viewer, and is the third version of
our medical image web viewers from the start. The summary of changes include:
- Addition of workflow modes
- Often, medical imaging use cases involves lots of specific workflows that re-use
functionalities. We have added the capability of workflow modes, that enable people
to customize user interface and configure application for specific workflow.
- The idea is to re-use the functionalities that extensions provide and create a workflow.
Brain segmentation workflow is different from prostate segmentation in UI for sure; however, they share the segmentation tools that can be re-used.
- Our vision is that technical people focus of developing extensions which provides
core functionalities, and experts to build modes by picking the appropriate functionalities
from each extension.
- Redux store has been removed from the viewer, and a cleaner, more powerful
- tailwind CSS
- End-to-end test suite
Below, you can find the gap analysis between the `OHIF-v2` and `OHIF-v3`:
{% include "./_gap_analysis.md" %}
## Where to next?
The Open Health Imaging Foundation intends to provide a simple general purpose The Open Health Imaging Foundation intends to provide a simple general purpose
DICOM Viewer which can be easily extended for specific uses. If you find DICOM Viewer which can be easily extended for specific uses. If you find
@ -33,7 +68,6 @@ yourself unable to extend the viewer for your purposes, please reach out via our
[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve [GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve
our integration and extension points. our integration and extension points.
## Where to next?
Check out these helpful links: Check out these helpful links:
@ -43,12 +77,19 @@ Check out these helpful links:
[Learn how you can be more involved.](./development/contributing.md) [Learn how you can be more involved.](./development/contributing.md)
- Feeling lost? Read our [help page](./help.md). - Feeling lost? Read our [help page](./help.md).
<!-- <!--
Links Links
--> -->
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[ohif-org]: http://www.ohif.org [ohif-org]: http://www.ohif.org
[ohif-demo]: http://viewer.ohif.org/
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb [dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
[gh-issues]: https://github.com/OHIF/Viewers/issues [gh-issues]: https://github.com/OHIF/Viewers/issues
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -1,61 +1,83 @@
# OHIF Viewers # OHIF Viewers
- [Our Process](our-process.md) - [Our Process](our-process.md)
- User Manuals
- [Overview](userManuals/index.md)
- [Study List](userManuals/studyList/index.md)
- [Viewer](userManuals/viewer/index.md)
- [Study Panel](userManuals/viewer/studyPanel/study-panel.md)
- [Measurement Panel](userManuals/viewer/measurementPanel/measurement-panel.md)
- [Viewport](userManuals/viewer/viewport/viewport.md)
- [Toolbar](userManuals/viewer/toolbar/toolbar.md)
- [Hot keys](userManuals/viewer/hotkeys/hotkeys.md)
- [Language](userManuals/viewer/language/language.md)
- Development - Development
- [Getting Started](development/getting-started.md) - [Getting Started](development/getting-started.md)
- [Contributing](development/contributing.md) - [Contributing](development/contributing.md)
- [Continuous Integration](development/continous-integration.md) - [Continuous Integration](development/continous-integration.md)
- [Testing](development/testing.md) - [Testing](development/testing.md)
- [Configuring](configuring/index.md) - [Configuring Data Source](configuring/index.md)
- [Data Source](configuring/data-source.md) - [Local DICOM Server](configuring/data-source.md)
--- ---
- [Architecture](architecture/index.md) - [Architecture](architecture/index.md)
- [Viewer](viewer/index.md) - [Viewer](viewer/index.md)
- [Configuration](viewer/configuration.md) - [Configuration](viewer/configuration.md)
- [Themeing](viewer/themeing.md) - [Theming](viewer/themeing.md)
- [Internationalization](viewer/internationalization.md) - [Internationalization](viewer/internationalization.md)
- [Extensions](extensions/index.md) - [Extensions](extensions/index.md#Overview)
- [Registering](extensions/index.md#registering-an-extension) - [Registering](extensions/index.md#registering-an-extension)
- [OHIF Maintained](extensions/index.md#ohif-maintained-extensions)
- [Lifecycle Hooks](extensions/index.md#lifecycle-hooks) - [Lifecycle Hooks](extensions/index.md#lifecycle-hooks)
- [preRegistration](extensions/lifecycle/pre-registration.md) - [preRegistration](extensions/lifecycle/pre-registration.md)
- [onModeEnter](extensions/lifecycle/on-mode-enter.md)
- [onModeExit](extensions/lifecycle/on-mode-exit.md)
- [Modules](extensions/index.md#modules) - [Modules](extensions/index.md#modules)
- [Commands](extensions/modules/commands.md)
- [Panel](extensions/modules/panel.md)
- [SOP Class Handler](extensions/modules/sop-class-handler.md)
- [Toolbar](extensions/modules/toolbar.md) - [Toolbar](extensions/modules/toolbar.md)
- [Commands](extensions/modules/commands.md)
- [DataSource](extensions/modules/data-source.md)
- [SOPClassHandler](extensions/modules/sop-class-handler.md)
- [Viewport](extensions/modules/viewport.md) - [Viewport](extensions/modules/viewport.md)
- [Contexts](extensions/index.md#contexts) - [Panel](extensions/modules/panel.md)
- [ExtensionManager](extensions/index.md#extensionmanager) - [LayoutTemplate](extensions/modules/layout-template.md)
- [OHIF Maintained](extensions/index.md#maintained-extensions) - [HangingProtocol](extensions/modules/hpModule.md)
- [Context](extensions/index.md#contexts)
- [ExtensionManager](managers/extension.md)
- [Modes](modes/index.md)
- [LifeCycle](modes/lifeCycle.md#overview)
- [Validity](modes/validity.md#overview)
- [Routes](modes/routes.md#overview)
- [Managers](managers/index.md)
- [Extension Manager](managers/extension.md)
- [Service Manager](managers/service.md)
- [Commands Manager](managers/commands.md)
- [Hotkeys Manager](managers/hotkeys.md)
- [Services](services/index.md) - [Services](services/index.md)
- [Default](services/default/index.md) - [Data Services](services/data/index.md)
- [UI](services/ui/index.md) - [Dicom Metadata Store](services/data/DicomMetadataStore.md)
- [Dialog Service](services/ui/ui-dialog-service.md) - [DisplaySet Service](services/data/DisplaySetService.md)
- [Modal Service](services/ui/ui-modal-service.md) - [Hanging Protocol Service](services/data/HangingProtocolService.md)
- [Notification Service](services/ui/ui-notification-service.md) - [Toolbar Service](services/data/ToolbarService.md)
- [Measurement Service](services/data/MeasurementService.md)
--- - [UI Services](services/ui/index.md)
- [UI Notification Service](services/ui/ui-notification-service.md)
- [UI Modal Service](services/ui/ui-modal-service.md)
- [UI Dialog Service](services/ui/ui-dialog-service.md)
- [UI Viewport Dialog Service](services/ui/ui-viewport-dialog-service.md)
- [Viewport Grid Service](services/ui/ViewportGridService.md)
- [Pub/Sub Pattern](services/pubsub.md)
- [Deployment](deployment/index.md) - [Deployment](deployment/index.md)
- [Embedded](deployment/index.md#embedded-viewer)
- [Stand-alone](deployment/index.md#stand-alone-viewer)
- [Data](deployment/index.md#data)
- Recipes
- Script Include
- [Embedding the Viewer](deployment/recipes/embedded-viewer.md)
- Stand-Alone
- [Build for Production](deployment/recipes/build-for-production.md) - [Build for Production](deployment/recipes/build-for-production.md)
- [Static](deployment/recipes/static-assets.md) - [Static](deployment/recipes/static-assets.md)
- [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md) - [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md)
- [User Account Control](deployment/recipes/user-account-control.md) - [User Account Control](deployment/recipes/user-account-control.md)
- [Google Cloud Healthcare](connecting-to-image-archives/google-cloud-healthcare.md) - [Google Cloud Healthcare](connecting-to-image-archives/google-cloud-healthcare.md)
---
- [FAQ](faq/index.md) - [FAQ](faq/index.md)
- [Scope of Project](faq/scope-of-project.md) - [Scope of Project](faq/scope-of-project.md)
- [Browser Support](faq/browser-support.md) - [Browser Support](faq/browser-support.md)
- [PWA vs Packaged](faq/pwa-vs-packaged.md) - [PWA vs Packaged](faq/pwa-vs-packaged.md)
- [Help](help.md) - [Help](help.md) -->

View File

@ -0,0 +1,110 @@
<table>
<thead>
<tr>
<th align="left" width="50%">OHIF-v2 functionalities</th>
<th align="center">OHIF-v3</th>
<th align="center">Comment</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">Rendering of 2D images via Cornerstone</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">Study List</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">Series Browser</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">DICOM JSON</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">2D Tools via CornerstoneTools</td>
<td align="center"></td>
<td align="center"></td>
</tr>
<tr>
<td align="left">OpenID Connect standard authentication flow for connecting to identity providers</td>
<td align="center">🔜</td>
<td align="center">In Progress</td>
</tr>
<tr>
<td align="left">Internationalization</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">Drag/drop DICOM data into the viewer (see https://viewer.ohif.org/local)</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">IHE Invoke Image Display - Standard-compliant launching of the viewer (e.g. from PACS or RIS)</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">DICOM PDF support</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">DICOM Whole-slide imaging viewport</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">Displaying non-renderable DICOM as HTML</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">Segmentation support</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">RT STRUCT support</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">White-labelling: Easily replace the OHIF Logo with your logo</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">DICOM upload to PACS</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">Google Cloud adapter</td>
<td align="center">🔜</td>
<td align="center">Not Started</td>
</tr>
<tr>
<td align="left">VTK Extension + MIP / MPR layout</td>
<td align="center"></td>
<td align="center">Other plans</td>
</tr>
<tr>
<td align="left">UMD Build (Embedded Viewer). </td>
<td align="center"></td>
<td align="center">The problem is that this breaks a bunch of extensions that rely on third party scripts (e.g. VTK) which have their own web worker loaders.</td>
</tr>
</tbody>
</table>
🔜

View File

@ -1,21 +1,27 @@
# Architecture # Architecture
Looking to extend your instance of the OHIF Viewer? Want learn how to reuse _a
portion_ of the Viewer in your own application? Or maybe you want to get
involved and draft or suggest a new feature? Regardless, you're in the right
place!
The OHIF Viewer aims to be decoupled, configurable, and extensible; while this >New `OHIF-v3` architecture has made OHIF a general purpose extensible medical imaging **platform**, as opposed to a somewhat highly configurable viewer.
allows our code to be used in more ways, it also increases complexity. Below, we
aim to demistify that complexity by providing insight into how our Viewer is In order to achieve a platform that can support various workflows and be extensible for the foreseeable future we went through extensive planning of possible use cases and decided to significantly change and improve the architecture.
Below, we aim to demystify that complexity by providing insight into how `OHIF Platform` is
architected, and the role each of it's dependent libraries plays. architected, and the role each of it's dependent libraries plays.
- [Architecture](#architecture)
- [Overview](#overview) - [Overview](#overview)
- [Business Logic](#business-logic) - [Extensions](#extensions)
- [Component Library](#react-component-library) - [Modes](#modes)
- [Extensions & Configuration](#extensions--configuration) - [Platform](#platform)
- [`@ohif/viewer`](#ohifviewer)
- [`@ohif/core`](#ohifcore)
- [`@ohif/ui`](#ohifui)
- [Overview of the architecture](#overview-of-the-architecture)
- [Common Questions](#common-questions) - [Common Questions](#common-questions)
## Overview ## Overview
The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a
@ -25,13 +31,17 @@ you'll see the following:
```bash ```bash
. .
├── extensions ├── extensions
│ ├── _example # Skeleton of example extension │ ├── _example # Skeleton of example extension
│ ├── default # default functionalities
│ ├── cornerstone # 2D images w/ Cornerstone.js │ ├── cornerstone # 2D images w/ Cornerstone.js
│ ├── dicom-html # Structured Reports as HTML in viewport │ ├── measurement-tracking # measurement tracking
│ ├── dicom-microscopy # Whole slide microscopy viewing │ ├── dicom-sr # Structured reports
│ ├── dicom-pdf # View DICOM wrapped PDFs in viewport │ └── dicom-pdf # View DICOM wrapped PDFs in viewport
│ └── vtk # MPR and Volume support w/ VTK.js
├── modes
│ └── longitudinal # longitudinal measurement tracking mode
├── platform ├── platform
│ ├── core # Business Logic │ ├── core # Business Logic
@ -45,90 +55,132 @@ you'll see the following:
└── README.md └── README.md
``` ```
The `platform` directory contains the business logic library, component library, OHIF v3 is comprised of the following components, described in detail in further sections:
and the application library that combines them to create a powerful medical
imaging viewer.
The `extensions` directory contains many packages that can be registered with - `@ohif/viewer`: The core framework that controls extension registration, mode composition and routing.
`@ohif/core`'s `ExtensionManager` to expand an application's supported features - `@ohif/core`: A library of useful and reusable medical imaging functionality for the web.
and functionality. - `@ohif/ui`: A library of reusable components to build OHIF-styled applications with.
- `Extensions`: A set of building blocks for building applications. The OHIF org maintains a few core libraries.
- `Modes`: Configuration objects that tell @ohif/viewer how to compose extensions to build applications on different routes of the platform.
![Architecture Diagram](../assets/img/architecture-diagram.png)
<center><i>architecture diagram</i></center>
This diagram is a conceptual illustration of how the Viewer is architected.
1. (optional) `extensions` can be registered with `@ohif/core`'s
`ExtensionManager`
2. `@ohif/core` provides bussiness logic and a way for `@ohif/viewer` to access
registered extensions
3. The `@ohif/viewer` composes and provides data to components from our
component library (`@ohif/ui`)
4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an
embeddable package ([`@ohif/viewer`][viewer-npm])
## Business Logic ## Extensions
The `extensions` directory contains many packages that provides essential
functionalities such as rendering, study/series browsers, measurement tracking that modes
can consume to enable a certain workflow. Extensions have had their behavior changed
in `OHIF-v3` and their api is expanded. In summary:
The [`@ohif/core`][core-github] project offers pre-packaged solutions for >In `OHIF-v3`, extensions no longer automatically hook themselves to the app. Now,
features common to Web-based medical imaging viewers. For example: >registering an extension makes its component available to `modes` that wish to use them.
> Basically, extensions in `OHIF-v3` are **building blocks** for building applications.
- Hotkeys
- DICOM Web requests
- Hanging Protocols
- Managing a study's measurements
- Managing a study's DICOM metadata
- [A flexible pattern for extensions](../extensions/index.md)
- And many others
It does this while remaining decoupled from any particular view library or OHIF team maintains several high value and commonly used functionalities in its
rendering logic. While we use it to power our React Viewer, it can be used with own extensions. For a list of extensions maintained by OHIF,
Vue, React, Vanilla JS, or any number of other frameworks.
## React Component Library
[`@ohif/ui`][ui-github] is a React Component library that contains the reusable
components that power the OHIF Viewer. It allows us to build, compose, and test
components in isolation; easing the development process by reducing the need to
stand-up a local PACS with test case data.
Extension authors can also use these same components when building their
extension's UI; allowing for a consistent look and feel with the rest of the
application.
[Check out our component library!](https://react.ohif.org/)
## Extensions & Configuration
While OHIF maintains several high value and commonly requested features in its
own extensions, there are many instances where one may wish to further extend
the viewer. Some common use cases include:
- Adding AI/ML tools and insights
- Custom workflows for guided diagnosis
- Collecting specific annotations for training data or reports
- Authentication and granular permissions
- Teleconsultation workflow, image comments, and tracking
- Adding surgical templating tools and reports
- and many others
We expose common integration points via [extensions](../extensions/index.md) to
make this possible. The viewer and many of our own extensions also offer
[configuration][configuration]. For a list of extensions maintained by OHIF,
[check out this helpful table](../extensions/index.md#maintained-extensions). [check out this helpful table](../extensions/index.md#maintained-extensions).
As an example `default` extension provides a default viewer layout,
a study/series browser and a datasource that maps to a DICOMWeb compliant backend.
If you find yourself thinking "I wish the Viewer could do X", and you can't
accomplish it with an extension today, create a GitHub issue! We're actively
looking for ways to improve our extensibility ^\_^
[Click here to read more about extensions!](../extensions/index.md) [Click here to read more about extensions!](../extensions/index.md)
## Modes
The `modes` directory contains workflows that can be registered with OHIF
within certain `routes`. The mode will get used once the user opens the viewer
on the registered route.
OHIF extensions were designed to provide certain core functionalities for building
your viewer. However, often in medical imaging we face a specific use case
in which we are using some core functionalities, adding our specific UI, and
use it in our workflows. Previously, to achieve this you had to create an
extension to add have such feature. `OHIF-v3` introduces `Modes` to enable
building such workflows by re-using the core functionalities from the extensions.
Some common workflows may include:
- Measurement tracking for lesions
- Segmentation of brain abnormalities
- AI probe mode for detecting prostate cancer
In the mentioned modes above, they will share the same core rendering module that
the `default` extension provides. However, segmentation mode will require
segmentation tools which is not needed for the other two.
As you can see, modes are a layer on top of extensions, that you
can configure in order to achieve certain workflows.
To summarize the difference between extensions and modes in `OHIF-v3` and extensions in `OHIF-v2`
> - `Modes` are configuration objects that tell *@ohif/viewer* how to compose extensions to build applications on different routes of the platform.
> - In v2 extensions are “plugins” that add functionality to a core viewer.
> - In v3 extensions are building blocks that a mode uses to build an entire viewer layout.
[Click here to read more about modes!](../modes/index.md)
## Platform
### `@ohif/viewer`
This library is the core library which consumes modes and extensions and builds an application. Extensions can be passed in as app configuration and will be consumed and initialized at the appropriate time by the application. Upon initialization the viewer will consume extensions and modes and build up the route desired, these can then be accessed via the study list, or directly via url parameters.
Upon release modes will also be plugged into the app via configuration, but this is still an area which is under development/discussion, and they are currently pulled from the window in beta.
Future ideas for this framework involve only adding modes and fetching the required extension versions at either runtime or build time, but this decision is still up for discussion.
### `@ohif/core`
OHIF core is a carefully maintained and tested set of web-based medical imaging functions and classes. This library includes managers and services used from within the viewer app.
OHIF core is largely similar to the @ohif/core library in v2, however a lot of logic has been moved to extensions:
however all logic about DICOMWeb and other data fetching mechanisms have been pulled out, as these now live in extensions, described later.
### `@ohif/ui`
Firstly, a large time-consumer/barrier for entry we discovered was building new UI in a timely manner that fit OHIFs theme. For this reason we have built a new UI component library which contains all the components one needs to build their own viewer.
These components are presentational only, so you can reuse them with whatever logic you desire. As the components are presentational, you may swap out @ohif/ui for a custom UI library with conforming API if you wish to white label the viewer. The UI library is here to make development easier and quicker, but it is not mandatory for extension components to use.
[Check out our component library!](https://react.ohif.org/)
## Overview of the architecture
OHIF-v3 architecture can be seen in the following figure. We will explore each
piece in more detail.
![mode-archs](../assets/img/mode-archs.png)
## Common Questions ## Common Questions
> When should I use the packaged source `@ohif/viewer` versus building a PWA
> from the source?
...
> Can I create my own Viewer using Vue.js or Angular.js? > Can I create my own Viewer using Vue.js or Angular.js?
@ -137,6 +189,14 @@ components. `@ohif/core` could still be used for business logic, and to provide
a model for extensions. `@ohif/ui` would then become a guide for the components a model for extensions. `@ohif/ui` would then become a guide for the components
you would need to recreate. you would need to recreate.
> When I want to implement a functionality, should it be in the mode or in an extension?
This is a great question. Modes are designed to consume extensions, so you should implement
your functionality in one of the modules of your new extension, and let the mode to consume it.
This way, in future, if you needed another mode that utilizes the same functionality, you can easily
hook the extension to the new mode as well.
<!-- <!--
Links Links
--> -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 994 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

View File

@ -35,7 +35,7 @@
}, },
{ {
"value": "https://docs.ohif.org/history/v2/", "value": "https://docs.ohif.org/history/v2/",
"text": "Version 2.0.0", "text": "Version 2.0.0"
}, },
{ {
"value": "https://docs.ohif.org/", "value": "https://docs.ohif.org/",

View File

@ -1,19 +1,17 @@
# Data Source
After following the steps outlined in
[Getting Started](./../development/getting-started.md), you'll notice that the
OHIF Viewer has data for several studies and their images. You didn't add this
data, so where is it coming from?
By default, the viewer is configured to connect to a remote server hosted by the
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
the time may come when you want to develop using your own data either locally or
remotely.
## Set up a local DICOM server ## Set up a local DICOM server
> ATTENTION! Already have a remote or local server? Skip to the - [Set up a local DICOM server](#set-up-a-local-dicom-server)
> [configuration section](#configuration-learn-more) below. - [Requirements](#requirements)
- [Open Source DICOM Image Archives](#open-source-dicom-image-archives)
- [Running Orthanc](#running-orthanc)
- [Orthanc: Learn More](#orthanc-learn-more)
- [Connecting to Orthanc](#connecting-to-orthanc)
- [Configuration: Learn More](#configuration-learn-more)
- [Running DCM4CHEE](#running-dcm4chee)
ATTENTION! Already have a remote or local server? Skip to the
[configuration section](#configuration-learn-more) below.
While the OHIF Viewer can work with any data source, the easiest to configure While the OHIF Viewer can work with any data source, the easiest to configure
are the ones that follow the [DICOMWeb][dicom-web] spec. are the ones that follow the [DICOMWeb][dicom-web] spec.
@ -41,6 +39,27 @@ in command prompt or terminal_
> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy] > the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy]
> uses to proxy requests > uses to proxy requests
## Open Source DICOM Image Archives
There are a lot of options available to you to use as a local DICOM server. Here are some of the more popular ones:
| Archive | Installation |
| --------------------------------------------- | ---------------------------------- |
| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] |
| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] |
| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] |
| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client |
| [Horos][horos] (**Mac OSX only**) | Desktop Client |
_Feel free to make a Pull Request if you want to add to this list._
Below, we will focus on `DCM4CHEE` and `Orthanc` usage:
### Running Orthanc ### Running Orthanc
_Start Orthanc:_ _Start Orthanc:_
@ -65,7 +84,7 @@ You can see the `docker-compose.yml` file this command runs at
[`<project-root>/.docker/Nginx-Orthanc/`][orthanc-docker-compose], and more on [`<project-root>/.docker/Nginx-Orthanc/`][orthanc-docker-compose], and more on
Orthanc for Docker in [Orthanc's documentation][orthanc-docker]. Orthanc for Docker in [Orthanc's documentation][orthanc-docker].
### Connecting to Orthanc #### Connecting to Orthanc
Now that we have a local Orthanc instance up and running, we need to configure Now that we have a local Orthanc instance up and running, we need to configure
our web application to connect to it. Open a new terminal window, navigate to our web application to connect to it. Open a new terminal window, navigate to
@ -88,7 +107,7 @@ yarn run dev:orthanc
> [Essentials Configuration](./index.md) guide. > [Essentials Configuration](./index.md) guide.
Let's take a look at what's going on under the hood here. `yarn run dev:orthanc` Let's take a look at what's going on under the hood here. `yarn run dev:orthanc`
is running the `dev:orthanc` script in our project's `package.json`. That script is running the `dev:orthanc` script in our project's `package.json` (inside `platform/viewer`). That script
is: is:
```js ```js
@ -114,51 +133,54 @@ configuration looks like:
```js ```js
window.config = { window.config = {
routerBasename: '/', routerBasename: '/',
servers: { extensions: [],
dicomWeb: [ modes: [],
showStudyList: true,
dataSources: [
{ {
name: 'Orthanc', friendlyName: 'dcmjs DICOMWeb Server',
wadoUriRoot: 'http://localhost:8899/wado', namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
qidoRoot: 'http://localhost:8899/dicom-web', sourceName: 'dicomweb',
wadoRoot: 'http://localhost:8899/dicom-web', configuration: {
qidoSupportsIncludeField: false, 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,
supportsReject: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
supportsWildcard: true,
},
}, },
], ],
}, defaultDataSourceName: 'dicomweb',
}; };
``` ```
To learn more about how you can configure the OHIF Viewer, check out our To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](./index.md). [Configuration Guide](./index.md).
## Open Source DICOM Image Archives
Our example uses `Orthanc`, but there are a lot of options available to you.
Here are some of the more popular ones:
| Archive | Installation | ### Running DCM4CHEE
| --------------------------------------------- | ---------------------------------- |
| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] | dcm4che is a collection of open source applications for healthcare enterprise written in Java programming language
| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] | which implements DICOM standard. dcm4chee (extra 'e' at the end) is dcm4che project
| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] | for an Image Manager/Image Archive which provides storage, retrieval and other functionalities.
| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client | You can read more about dcm4chee in their website [here](https://www.dcm4che.org/)
| [Horos][horos] (**Mac OSX only**) | Desktop Client |
DCM4chee installation is out of scope for these tutorials and can be found [here](https://github.com/dcm4che/dcm4chee-arc-light/wiki/Run-minimum-set-of-archive-services-on-a-single-host)
An overview of steps for running OHIF Viewer using a local DCM4CHEE is shown below:
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/557570043?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Local dcm4chee"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
_Feel free to make a Pull Request if you want to add to this list._
<!--
Links
-->
<!-- prettier-ignore-start -->
[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
[storescu]: http://support.dcmtk.org/docs/storescu.html
[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy
[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/.docker/Nginx-Orthanc
<!-- Archives -->
[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light [dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light
[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker [dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
[orthanc]: https://www.orthanc-server.com/ [orthanc]: https://www.orthanc-server.com/
@ -167,4 +189,6 @@ _Feel free to make a Pull Request if you want to add to this list._
[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code [dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code
[osirix]: http://www.osirix-viewer.com/ [osirix]: http://www.osirix-viewer.com/
[horos]: https://www.horosproject.org/ [horos]: https://www.horosproject.org/
<!-- prettier-ignore-end --> [default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js
[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates
[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config

View File

@ -1,17 +1,27 @@
# Configuration # Configuring Data Source
> This step assumes you have an imaging archive. If you need assistance setting
> one up, check out the [`Data Source` Guide](./data-source.md) or a deployment
> recipe that contains an open Image Archive
- [Configuring Data Source](#configuring-data-source)
- [Overview](#overview) - [Overview](#overview)
- [Configuration Files](#configuration-files) - [Configuration Files](#configuration-files)
- [Environment Variables](#environment-variables) - [Environment Variables](#environment-variables)
- [How do I configure my project?](#how-do-i-configure-my-project)
## Overview ## Overview
### Configuration Files After following the steps outlined in
[Getting Started](./../development/getting-started.md), you'll notice that the
OHIF Viewer has data for several studies and their images. You didn't add this
data, so where is it coming from?
By default, the viewer is configured to connect to a remote server hosted by the
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
the time may come when you want to develop using your own data either locally or
remotely.
## Configuration Files
The configuration for our viewer is in the `<root>platform/viewer/public/config` The configuration for our viewer is in the `<root>platform/viewer/public/config`
directory. Our build process knows which configuration file to use based on the directory. Our build process knows which configuration file to use based on the
@ -19,55 +29,57 @@ directory. Our build process knows which configuration file to use based on the
[`config/default.js`][default-config]. The majority of the viewer's features, [`config/default.js`][default-config]. The majority of the viewer's features,
and registered extension's features, are configured using this file. and registered extension's features, are configured using this file.
**Embedded Use Note:**
Alternatively, when using the `umd` bundle for embedded use cases, these same
values are what you'll pass to `installViewer` method:
`OHIFStandaloneViewer.installViewer(window.config)`
### Environment Variables
We use environment variables at build and dev time to change the Viewer's
behavior. We can update the `HTML_TEMPLATE` to easily change which extensions
are registered, and specify a different `APP_CONFIG` to connect to an
alternative data source (or even specify different default hotkeys).
| Environment Variable | Description | Default |
| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- |
| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` |
| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` |
| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` |
| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
## How do I configure my project?
The simplest way is to update the existing default config: The simplest way is to update the existing default config:
_/platform/viewer/public/config/default.js_ > _/platform/viewer/public/config/default.js_
```js ```js
window.config = { window.config = {
routerBasename: '/', routerBasename: '/',
servers: { extensions: [],
dicomWeb: [ modes: [],
showStudyList: true,
dataSources: [
{ {
friendlyName: 'dcmjs DICOMWeb Server',
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb',
configuration: {
name: 'DCM4CHEE', name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
supportsReject: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
supportsWildcard: true,
},
}, },
], ],
}, defaultDataSourceName: 'dicomweb',
}; };
``` ```
> As you can see a new change in `OHIF-v3` is the addition of `dataSources`.
> You can build your own datasource and map it to the internal data structure of OHIFs > metadata and enjoy using other peoples developed mode on your own data!
>
> You can read more about data sources at [Data Source section in Modes](../modes/index.md)
>
The configuration can also be written as a JS Function in case you need to inject dependencies like external services: The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
```js ```js
window.config = ({ servicesManager } = {}) => { window.config = ({ servicesManager } = {}) => {
const { UIDialogService } = servicesManager.services; const { UIDialogService } = servicesManager.services;
@ -82,23 +94,59 @@ window.config = ({ servicesManager } = {}) => {
}, },
}, },
routerBasename: '/', routerBasename: '/',
servers: { dataSources: [
dicomWeb: [
{ {
friendlyName: 'dcmjs DICOMWeb Server',
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
sourceName: 'dicomweb',
configuration: {
name: 'DCM4CHEE', name: 'DCM4CHEE',
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado', wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
qidoSupportsIncludeField: true, qidoSupportsIncludeField: true,
supportsReject: true,
imageRendering: 'wadors', imageRendering: 'wadors',
thumbnailRendering: 'wadors', thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
supportsWildcard: true,
},
}, },
], ],
}, defaultDataSourceName: 'dicomweb',
}; };
}; };
``` ```
<!-- **Embedded Use Note:**
Alternatively, when using the `umd` bundle for embedded use cases, these same
values are what you'll pass to `installViewer` method:
`OHIFStandaloneViewer.installViewer(window.config)` -->
## Environment Variables
We use environment variables at build and dev time to change the Viewer's
behavior. We can update the `HTML_TEMPLATE` to easily change which extensions
are registered, and specify a different `APP_CONFIG` to connect to an
alternative data source (or even specify different default hotkeys).
| Environment Variable | Description | Default |
| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- |
| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` |
| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` |
| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` |
| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
You can also create a new config file and specify its path relative to the build 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 output's root by setting the `APP_CONFIG` environment variable. You can set the
value of this environment variable a few different ways: value of this environment variable a few different ways:
@ -115,11 +163,27 @@ value of this environment variable a few different ways:
After updating the configuration, `yarn run build` to generate updated build After updating the configuration, `yarn run build` to generate updated build
output. output.
<!-- <!--
Links Links
--> -->
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
[storescu]: http://support.dcmtk.org/docs/storescu.html
[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy
[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/.docker/Nginx-Orthanc
<!-- Archives -->
[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light
[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
[orthanc]: https://www.orthanc-server.com/
[orthanc-docker]: http://book.orthanc-server.com/users/docker.html
[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud
[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code
[osirix]: http://www.osirix-viewer.com/
[horos]: https://www.horosproject.org/
[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js [default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js
[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates [html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates
[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config [config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config

View File

@ -1,71 +1,3 @@
# Google Cloud Healthcare # Google Cloud Healthcare
> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a > Coming soon - We are working on bringing Google Cloud Healthcare to OHIF-v3
> powerful option for storing medical imaging data in the cloud.
An alternative to deploying your own PACS is to use a software-as-a-service
provider such as Google Cloud. The Cloud Healthcare API promises to be a
scalable, secure, cost effective image storage solution for those willing to
store their data in the cloud. It offers an
[almost-entirely complete DICOMWeb API](https://cloud.google.com/healthcare/docs/dicom)
which requires tokens generated via the
[OAuth 2.0 Sign In flow](https://developers.google.com/identity/sign-in/web/sign-in).
Images can even be transcoded on the fly if this is desired.
## Setup a Google Cloud Healthcare Project
- Create a Google Cloud account
- Create a project in Google Cloud
- Enable the [Cloud Healthcare API](https://cloud.google.com/healthcare/) for
your project.
- (Optional): Create a Dataset and Data Store for storing your DICOM data
- Enable the
[Cloud Resource Manager API](https://cloud.google.com/resource-manager/) for
your project.
- _Note:_ If you are having trouble finding the APIs, use the search box at
the top of the Cloud console.
- Go to APIs & Services > Credentials to create an OAuth Consent screen and fill
in your application details.
- Under Scopes for Google APIs, click "manually paste scopes".
- Add the following scopes:
- `https://www.googleapis.com/auth/cloudplatformprojects.readonly`
- `https://www.googleapis.com/auth/cloud-healthcare`
- Go to APIs & Services > Credentials to create a new set of credentials:
- Choose the "Web Application" type
- Set up an
[OAuth 2.0 Client ID](https://support.google.com/cloud/answer/6158849?hl=en)
- Add your domain (e.g. `http://localhost:3000`) to Authorized JavaScript
origins.
- Add your domain, plus `callback` (e.g. `http://localhost:3000/callback`) to
Authorized Redirect URIs.
- Save your Client ID for later.
- (Optional): Enable Public Datasets that are being hosted by Google:
https://cloud.google.com/healthcare/docs/resources/public-datasets/
## Run the viewer with your OAuth Client ID
1. Open the `config/google.js` file and change `YOURCLIENTID` to your Client ID
value.
1. Run the OHIF Viewer using the config/google.js configuration file
```bash
cd OHIFViewer
yarn install
APP_CONFIG=config/google.js yarn run dev
```
## Running via Docker
The OHIF Viewer Docker container can be connected to Google Cloud Healthcare by
providing a Client ID at runtime. This is a very simple method to get up and
running.
1. Install Docker (https://www.docker.com/)
1. Run the Docker container, providing a Client ID as an environment variable.
Client IDs look like `xyz.apps.googleusercontent.com`.
```bash
docker run --env CLIENT_ID=$CLIENT_ID --publish 5000:80 ohif/viewer:latest
```

View File

@ -16,17 +16,10 @@ applications. If you find yourself a little lost, please don't hesitate to
## Deployment Scenarios ## Deployment Scenarios
### Embedded Viewer ### Embedded Viewer (deprecated)
`OHIF-v3` has deprecated deploying the viewer as an embedded viewer the number of underlying
The quickest and easiest way to get the OHIF Viewer up and running is to embed libraries that run web workers are increasing for OHIF. An example of these libraries is
it into an existing web application. It allows us to forego a "build step", and OHIF's 3D rendering functionality that is provided by `vtk-js`.
add a powerful medical imaging viewer to an existing web page using only a few
include tags.
- Read more about it here: [Embedded Viewer](./recipes/embedded-viewer.md)
- And check out our [live demo on CodeSandbox][code-sandbox]
{% include "./_embedded-viewer-diagram.md" %}
### Stand-alone Viewer ### Stand-alone Viewer
@ -35,7 +28,7 @@ benefits, but comes at the cost of time and complexity. Some benefits include:
_Today:_ _Today:_
- Leverage [extensions](/extensions/index.md) to drop-in powerful new features - Leverage [extensions](/extensions/index.md) and [modes](/modes/index.md) to drop-in powerful new features
- Add routes and customize the viewer's workflow - Add routes and customize the viewer's workflow
- Finer control over styling and whitelabeling - Finer control over styling and whitelabeling
@ -101,7 +94,7 @@ the steps layed out in our
#### What if I don't have an imaging archive? #### What if I don't have an imaging archive?
We provide some guidance on configuring a local image archive in our We provide some guidance on configuring a local image archive in our
[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an [Data Source Essentials](./../configuring/index.md#set-up-a-local-DICOM-server) guide. Hosting an
archive remotely is a little trickier. You can check out some of our archive remotely is a little trickier. You can check out some of our
[advanced recipes](#recipes) for modeled setups that may work for you. [advanced recipes](#recipes) for modeled setups that may work for you.
@ -136,12 +129,14 @@ It should reference an endpoint that returns **application/json** formatted text
If you do not have an API, you can simply return a text file containing the JSON from any web server. If you do not have an API, you can simply return a text file containing the JSON from any web server.
You tell the OHIF viewer to use JSON by appending the `'?url='` query to the `/Viewer` route: You tell the OHIF viewer to use JSON by using the `dicomjson` datasource and appending `'?url='` query to your mode's route:
eg. `https://my-test-ohif-server/myMode/dicomjson?url=https://my-json-server/study-uid.json`
eg. `https://my-test-ohif-server/viewer?url=https://my-json-server/study-uid.json`
The returned JSON object must contain a single root object with a 'studies' array. The returned JSON object must contain a single root object with a 'studies' array.
You can read more about using different data sources for mode's routes [here](../modes/routes.md#route-path)
*Sample JSON format:* *Sample JSON format:*
```JSON ```JSON
@ -199,11 +194,11 @@ More info on this JSON format can be found here [Issue #1500](https://github.com
**Implementation Notes:** **Implementation Notes:**
1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route. <!-- 1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route. -->
2. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomweb:` in order for the cornerstone image loader to retrieve it correctly. 1. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomjson:` in order for the cornerstone image loader to retrieve it correctly.
eg. `https://image-server/my-image.dcm` ---> `dicomweb:https://image-server/my-image.dcm` eg. `https://image-server/my-image.dcm` ---> `dicomjson:https://image-server/my-image.dcm`
3. The JSON format above is compatible with >= v3.7.8 of the application. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet. 2. The JSON format above is compatible with >= v3.7.8 of the application in `V2` version. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
4. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500). 3. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
*CORS Issues (Cross-Origin Resource Sharing)* *CORS Issues (Cross-Origin Resource Sharing)*
@ -219,7 +214,7 @@ Your JSON API is hosted on `https://my-json-api.aws.com`
And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com` And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
When you first start your application, browsing to `https://my-ohif-server.com/viewer?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`. When you first start your application, browsing to `https://my-ohif-server.com/myMode/dicomjson?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this. Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
@ -230,7 +225,9 @@ Essentially, whenever the application connects to a remote resource, you will ne
### Securing Your Data ### Securing Your Data
Coming soon
<!--
> Feeling lost? Securing your data is important, and it can be hard to tell if > Feeling lost? Securing your data is important, and it can be hard to tell if
> you've gotten it right. Don't hesitate to work with professional auditors, or > you've gotten it right. Don't hesitate to work with professional auditors, or
> [enlist help from experts](./../help.md). > [enlist help from experts](./../help.md).
@ -273,15 +270,15 @@ In general, we recommend using the "Authorization Code Flow" ( [see
taken. If the flow you've chosen produces a JWT Token, it's validity can be used taken. If the flow you've chosen produces a JWT Token, it's validity can be used
to secure access to your Image Archive as well. to secure access to your Image Archive as well.
-->
### Recipes ### Recipes
We've included a few recipes for common deployment scenarios. There are many, We've included a few recipes for common deployment scenarios. There are many,
many possible configurations, so please don't feel limited to these setups. many possible configurations, so please don't feel limited to these setups.
Please feel free to suggest or contribute your own recipes. Please feel free to suggest or contribute your own recipes.
- Script Include
- [Embedding the Viewer](./recipes/embedded-viewer.md)
- Stand-Alone
- [Build for Production](./recipes/build-for-production.md) - [Build for Production](./recipes/build-for-production.md)
- [Static](./recipes/static-assets.md) - [Static](./recipes/static-assets.md)
- [Nginx + Image Archive](./recipes/nginx--image-archive.md) - [Nginx + Image Archive](./recipes/nginx--image-archive.md)

View File

@ -104,14 +104,18 @@ directory:
// Install http-server as a globally available package // Install http-server as a globally available package
yarn global add http-server yarn global add http-server
// Change the directory to the platform/viewer
// Serve the files in our current directory // Serve the files in our current directory
// Accessible at: `http://localhost:8080` // Accessible at: `http://localhost:8080`
http-server npx http-server ./dist
``` ```
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/551957266?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="HTTP Server Deploy"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
### Automating Builds and Deployments ### Automating Builds and Deployments
If you found setting up your environmnent and running all of these steps to be a If you found setting up your environment and running all of these steps to be a
bit tedious, then you are in good company. Thankfully, there are a large number bit tedious, then you are in good company. Thankfully, there are a large number
of tools available to assist with automating tasks like building and deploying of tools available to assist with automating tasks like building and deploying
web application. For a starting point, check out this repository's own use of: web application. For a starting point, check out this repository's own use of:
@ -120,11 +124,7 @@ web application. For a starting point, check out this repository's own use of:
- [Netlify][netlify]: [netlify.toml][netlify.toml] | - [Netlify][netlify]: [netlify.toml][netlify.toml] |
[build-deploy-preview.sh][build-deploy-preview.sh] [build-deploy-preview.sh][build-deploy-preview.sh]
## Troubleshooting
> Issues and resolutions for common GitHub issues will be summarized here
...
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[circleci]: https://circleci.com/gh/OHIF/Viewers [circleci]: https://circleci.com/gh/OHIF/Viewers

View File

@ -1,171 +0,0 @@
# Embedded Viewer
The quickest and easiest way to get the OHIF Viewer up and running is to embed
it into an existing web application. It allows us to forego a "build step", and
add a powerful medical imaging viewer to an existing web page using only a few
include tags. Here's how it works:
{% include "./../_embedded-viewer-diagram.md" %}
1. Create a new web page or template that includes the following external
dependencies:
<ul>
<li>
<a href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700&display=swap">
<code>Google Font: Roboto</code>
</a>
</li>
<li>
<a href="https://unpkg.com/@ohif/viewer">
<code>@ohif/viewer@latest</code>
</a>
</li>
</ul>
<ol start="2">
<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` (JS Object)
window.config = {
// default: '/'
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',
},
],
},
};
```
To learn more about how you can configure the OHIF Viewer, check out our
[Configuration Guide](../../configuring/index.md).
<ol start="3"><li>
Render the viewer in the web page's target <code>div</code>
</li></ol>
```js
// Made available by the `@ohif/viewer` script included in step 1
var containerId = 'id-of-div-to-render-component-to';
var componentRenderedOrUpdatedCallback = function() {
console.log('OHIF Viewer rendered/updated');
};
window.OHIFViewer.installViewer(
window.config,
containerId,
componentRenderedOrUpdatedCallback
);
```
You can see a live example of this recipe in [this CodeSandbox][code-sandbox].
## Add Extensions
The UMD build of the OHIF Viewer is a "light weight" build that only contains
the core extensions required for basic 2D image viewing. It's possible to add
other extensions at runtime.
This only requires us to include a single script tag, and add it using the
`extensions` key to our config. In this practical example, we register our
popular whole slide microscopy extension:
```html
<script
src="https://unpkg.com/@ohif/extension-dicom-microscopy@0.50.5/dist/index.umd.js"
crossorigin
></script>
<!-- --->
<script>
window.config = {
// ...
extensions: [OHIFExtDicomMicroscopy],
};
</script>
```
You can see an example of a slide microscopy study in the viewer [with the
extension enabled here][whole-slide-ext-demo] ([source code][ext-code-sandbox])
and [without it here][whole-slide-base-demo] ([source code][code-sandbox]).
You can read more about extensions and how to create your own in our
[extensions guide](/extensions/index.md).
#### FAQ
> I'm having trouble getting this to work. Where can I go for help?
First, check out this fully functional [CodeSandbox][code-sandbox] example. If
you're still having trouble, feel free to search or GitHub issues. Can't find
anything related your problem? Create a new one.
> My application's styles are impacting the OHIF Viewer's look and feel. What
> can I do?
When you include stylesheets and scripts, they are added globally. This has the
potential of causing conflicts with other scripts and styles on the page. To
prevent this, `embed` the viewer in a new/empty web page. Have that working?
Good. Now `embed` that new page using an
[`<iframe>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe).
This should produce the expected result while also protecting your page from any
globally defined styles/scripts.
> We're trying to embed the OHIF Viewer into an existing React App, but seeing
> react-dom and react conflicts. What can we do?
If you are installing OHIF viewer inside another react app, you may use `installViewer` as follows:
```
import { installViewer } from '@ohif/viewer'
const ohifViewerConfig = window.config // or set it here
const containerId = 'ohif'
const componentRenderedOrUpdatedCallback = function() {
console.log('OHIF Viewer rendered/updated');
};
componentDidMount() {
installViewer(
ohifViewerConfig,
containerId,
componentRenderedOrUpdatedCallback
);
}
render () {
...
//you can render in any element you wish
<AnyTag id={containerId}/>
}
```
`installViewer` is a convenience method that pulls in some dependencies that may
not be compatible with existing `react` apps. `@ohif/viewer` also exports `App`
which is a react component that takes the `configuration` outlined above as
props. You can use it as a reusable component, and to avoid `react` version
conflict issues.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
[whole-slide-base-demo]: https://tprch.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2020.1519719354.757
[ext-code-sandbox]: https://codesandbox.io/s/viewer-script-tag-microscopy-extension-44unk
[whole-slide-ext-demo]: https://44unk.csb.app/viewer/1.2.392.200140.2.1.1.1.2.799008771.2448.1519719572.518
<!-- prettier-ignore-end -->

View File

@ -79,6 +79,20 @@ in command prompt or terminal_
### Setup ### Setup
- Navigate to `viewer` folder inside `platform`
- then: `cd .recipes/OpenResty-Orthanc`
- run: `docker-compose up --build`
- Navigate to `127.0.0.1` for the viewer
- Navigate to `127.0.0.1/pacs-admin` for uploading studies
You can see the overview of the mentioned steps:
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/554726410?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="NginX Deployment"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
<!-- ### Setup
_Spin Things Up_ _Spin Things Up_
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell - Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
@ -90,7 +104,7 @@ _Upload Your First Study_
- From the top right, select "Upload" - From the top right, select "Upload"
- Click "Select files to upload..." (DICOM) - Click "Select files to upload..." (DICOM)
- Click "Start the upload" - Click "Start the upload"
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List - Navigate back to `http://127.0.0.1/` to view your studies in the Study List -->
### Troubleshooting ### Troubleshooting
@ -110,6 +124,14 @@ Stop running all containers:
- Win: `docker ps -a -q | ForEach { docker stop $_ }` - Win: `docker ps -a -q | ForEach { docker stop $_ }`
- Linux: `docker stop $(docker ps -a -q)` - Linux: `docker stop $(docker ps -a -q)`
_Traceback (most recent call last):_
_File "urllib3/connectionpool.py", line 670, in urlopen_
_...._
Are you sure your docker is running? see explanation [here](https://github.com/docker/compose/issues/7896)
### Configuration ### Configuration
After verifying that everything runs with default configuration values, you will After verifying that everything runs with default configuration values, you will

View File

@ -25,6 +25,13 @@ _Easy_
_Advanced_ _Advanced_
- [Deploy Static Assets](#deploy-static-assets)
- [Drag-n-drop](#drag-n-drop)
- [Netlify: Drop](#netlify-drop)
- [Easy](#easy)
- [Surge.sh](#surgesh)
- [GitHub Pages](#github-pages)
- [Advanced](#advanced)
- [AWS S3 + Cloudfront](#aws-s3--cloudfront) - [AWS S3 + Cloudfront](#aws-s3--cloudfront)
- [GCP + Cloudflare](#gcp--cloudflare) - [GCP + Cloudflare](#gcp--cloudflare)
- [Azure](#azure) - [Azure](#azure)
@ -33,11 +40,8 @@ _Advanced_
### Netlify: Drop ### Netlify: Drop
> Build, deploy, and manage modern web projects. An all-in-one workflow that <div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/551957463?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Netlify Drop"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
> combines global deployment, continuous integration, and automatic HTTPS. And
> thats just the beginning.
![netlify-drop example](../../assets/img/netlify-drop.gif)
_GIF demonstrating deployment with Netlify Drop_ _GIF demonstrating deployment with Netlify Drop_

View File

@ -1,288 +1,3 @@
# User Account Control # User Account Control
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in > Coming soon - We are working on bringing the User Account Control to OHIF-v3
> doubt, enlist the help of an expert and conduct proper audits.
Making a viewer and its medical imaging data accessible on the open web can
provide a lot of benefits, but requires additional security to make sure
sensitive information can only be viewed by authorized individuals. Most image
archives are equipped with basic security measures, but they are not
robust/secure enough for the open web.
This guide covers one of many potential production setups that secure our
sensitive data.
## Overview
This guide builds on top of our
[Nginx + Image Archive guide](/deployment/recipes/nginx--image-archive.md),
wherein we used a [`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy)
to retrieve resources from our image archive (Orthanc).
To add support for "User Account Control" we introduce
[Keycloak](https://www.keycloak.org/about.html). Keycloak is an open source
Identity and Access Management solution that makes it easy to secure
applications and services with little to no code. We improve upon our
`reverse proxy` setup by integrating Keycloak and Nginx to create an
`authenticating reverse proxy`.
> An authenticating reverse proxy is a reverse proxy that only retrieves the
> resources on behalf of a client if the client has been authenticated. If a
> client is not authenticated they can be redirected to a login page.
This setup allows us to create a setup similar to the one pictured below:
{% include "./../_user-account-control-flow-diagram.md" %}
- All web requests are routed through `nginx` on our `OpenResty` image
- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints
- Requires valid `Authorization: Bearer <token>` header
- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin
- `/auth` is a reverse proxy for `keycloak`
- All static resources for OHIF Viewer are unprotected and accessible. We have
application logic that will redirect unauthenticated users to the appropriate
`keycloak` login screen.
## Getting Started
### Requirements
- Docker
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
_Not sure if you have `docker` installed already? Try running `docker --version`
in command prompt or terminal_
### Setup
_Spin Things Up_
- Navigate to `<project-root>/docker/OpenResty-Orthanc-Keycloak` in your shell
- Run `docker-compose up`
_Create Your First User_
- Navigate to: `http://127.0.0.1/auth/admin`
- Sign in with: `admin`/`password`
- From the top left dropdown, select the `Ohif` realm
- From the left sidebar, under `Manage`, select `Users`
- Click `Add User`
- Username: `test`
- Email Verified: `ON`
- Click `Save`
- Click the `Credentials` Tab
- New Pasword: `test`
- Password Confirmation: `test`
- Temporary: `OFF`
- Click: `Reset Password`
- From the top right dropdown, select `Admin`, then `Sign Out`
_Sign In_
- Navigate to `http://127.0.0.1/`
- Username: `test`, Password: `test`
- Click `Log In`
_Upload Your First Study_
- Navigate to `http://127.0.0.1/pacs-admin`
- If you're not already logged in, use `test`/`test`
- From the top right, select "Upload"
- Click "Select files to upload..." (DICOM)
- Click "Start the upload"
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List
### Troubleshooting
_Exit code 137_
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
tab, and increase the amount of Memory available.
_Cannot create container for service X_
Use this one with caution: `docker system prune`
_X is already running_
Stop running all containers:
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
- Linux: `docker stop $(docker ps -a -q)`
### Configuration
After verifying that everything runs with default configuration values, you will
likely want to update:
- The domain: `http://127.0.0.1`
- Set secure, non-default passwords
- Regenerate Keycloak Client Secrets
#### OHIF Viewer
The OHIF Viewer's configuration is imported from a static `.js` file. The
configuration we use is set to a specific file when we build the viewer, and
determined by the env variable: `APP_CONFIG`. You can see where we set its value
in the `dockerfile` for this solution:
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
You can find the configuration we're using here:
`/public/config/docker_openresty-orthanc-keycloak.js`
To rebuild the `webapp` image created by our `dockerfile` after updating the
Viewer's configuration, you can run:
- `docker-compose build` OR
- `docker-compose up --build`
#### Other
All other files are found in: `/docker/OpenResty-Orthanc-Keycloak/`
| Service | Configuration | Docs |
| ----------------- | ------------------------------------------------ | ------------------------------------------- |
| OHIF Viewer | [dockerfile][dockerfile] / [config.js][config] | You're reading them now! |
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
| Keycloak | [`/ohif-keycloak-realm.json`][config-keycloak]\* | |
\* These are the seed values for Keycloak. They can be manually updated at
`http://127.0.0.1/auth/admin`
#### Keycloak Themeing
The `Login` screen for the `ohif-viewer` client is using a Custom Keycloak
theme. You can find the source files for it in
`/docker/OpenResty-Orthanc-Keycloak/volumes/keycloak-themes/`. You can see how
we add it to Keycloak in the `docker-compose` file, and you can read up on how
to leverage custom themes in
[Keycloak's own docs](https://www.keycloak.org/docs/latest/server_development/index.html#_themes).
| Default Theme | OHIF Theme |
| ---------------------------------------------------------------------- | ---------------------------------------------------------------- |
| ![Keycloak Default Theme](../../assets/img/keycloak-default-theme.png) | ![Keycloak OHIF Theme](../../assets/img/keycloak-ohif-theme.png) |
## Next Steps
### Deploying to Production
While these configuration and docker-compose files model an environment suitable
for production, they are not easy to deploy "as is". You can either:
- Manually recreate this environment and deploy built application files **OR**
- Deploy to a cloud kubernetes provider like
[Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR**
- [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category)
- Find and follow your preferred provider's guide on setting up
[swarms and stacks](https://docs.docker.com/get-started/)
### Adding SSL
Adding SSL registration and renewal for your domain with Let's Encrypt that
terminates at Nginx is an incredibly important step toward securing your data.
Here are some resources, specific to this setup, that may be helpful:
- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl)
- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)
While we terminate SSL at Nginx, it may be worth using self signed certificates
for communication between services.
- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/)
### Use PostgresSQL w/ Orthanc
Orthanc can handle a large amount of data and requests, but if you find that
requests start to slow as you add more and more studies, you may want to
configure your Orthanc instance to use PostgresSQL. Instructions on how to do
that can be found in the
[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under
"PostgreSQL and Orthanc inside Docker"
### Improving This Guide
Here are some improvements this guide would benefit from, and that we would be
more than happy to accept Pull Requests for:
- SSL Support
- Complete configuration with `.env` file (or something similar)
- Keycloak Theme improvements
- Any security issues
- One-click deploy to a cloud provider
## Resources
### Misc. Helpful Commands
_Check if `nginx.conf` is valid:_
```bash
docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t
```
_Interact w/ running container:_
`docker exec -it CONTAINER_NAME bash`
_List running containers:_
`docker ps`
_Clear Keycloak DB so you can re-seed values:_
- `docker volume prune` OR
- `docker volume ls` and `docker volume rm VOLUME_NAME VOLUME_NAME`
### Referenced Articles
The inspiration for our setup was driven largely by these articles:
- [Securing Nginx with Keycloak](https://edhull.co.uk/blog/2018-06-06/keycloak-nginx)
- [Authenticating Reverse Proxy with Keycloak](https://eclipsesource.com/blogs/2018/01/11/authenticating-reverse-proxy-with-keycloak/)
- [Securing APIs with Kong and Keycloak](https://www.jerney.io/secure-apis-kong-keycloak-1/)
For more documentation on the software we've chosen to use, you may find the
following resources helpful:
- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html)
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
- [Auth0: Picking a Grant Type](https://auth0.com/docs/api-auth/which-oauth-flow-to-use)
We chose to use a generic OpenID Connect library on the client, but it's worth
noting that Keycloak comes packaged with its own:
- [redux-oidc](https://github.com/maxmantz/redux-oidc) (Which wraps
[oidc-client-js](https://github.com/IdentityModel/oidc-client-js/wiki))
- [Keycloak JavaScript Adapter](https://www.keycloak.org/docs/latest/securing_apps/index.html#_javascript_adapter)
If you're not already drowning in links, here are some good security resources
for OAuth:
- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660)
- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code)
For a different take on this setup, check out the repositories our community
members put together:
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
<!--
Links
-->
<!-- prettier-ignore-start -->
<!-- DOCS -->
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
<!-- SRC -->
[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json
[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json
<!-- prettier-ignore-end -->

View File

@ -43,11 +43,13 @@ On your local file system:
- Open a terminal/shell - Open a terminal/shell
- Navigate to `cornerstonejs/cornerstone-tools` - Navigate to `cornerstonejs/cornerstone-tools`
- `npm install` - `yarn install`
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link) - [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
- `npm run dev` - `yarn run dev`
- Open a new terminal/shell - Open a new terminal/shell
- Navigate to `ohif/viewers`. - Navigate to `ohif/viewers` (the root of ohif project)
- `yarn install` - `yarn install`
- [`yarn link cornerstone-tools`](https://yarnpkg.com/en/docs/cli/link) - [`yarn link cornerstone-tools`](https://yarnpkg.com/en/docs/cli/link)
- `yarn run dev` - `yarn run dev`
@ -68,6 +70,7 @@ If you wish to stop using your local package, run the following commands in the
- `yarn unlink cornerstone-tools` - `yarn unlink cornerstone-tools`
- `yarn install --force` - `yarn install --force`
<!--
### `react-vtkjs-viewport` ### `react-vtkjs-viewport`
On your local file system: On your local file system:
@ -88,7 +91,7 @@ On your local file system:
- Navigate to `ohif/viewers`. - Navigate to `ohif/viewers`.
- `yarn install` - `yarn install`
- [`yarn link react-vtkjs-viewport`](https://yarnpkg.com/en/docs/cli/link) - [`yarn link react-vtkjs-viewport`](https://yarnpkg.com/en/docs/cli/link)
- `yarn run dev` - `yarn run dev` -->
#### Other linkage notes #### Other linkage notes
@ -97,6 +100,10 @@ there are a lot of factors that can influence the behavior of our development
server and bundler. If you encounter issues not addressed here, please don't server and bundler. If you encounter issues not addressed here, please don't
hesitate to reach out on GitHub. hesitate to reach out on GitHub.
Sometimes you might encounter a situation where the linking doesn't work as expected.
This might happen when there are multiple linked packages with the same name.
You can [remove][unlink] the linked packages inside yarn and try again.
## Any guidance on submitting changes? ## Any guidance on submitting changes?
While we do appreciate code contributions, triaging and integrating contributed While we do appreciate code contributions, triaging and integrating contributed
@ -112,7 +119,7 @@ working on your pull requests:
We will provide feedback on your pull requests as soon as possible. Following We will provide feedback on your pull requests as soon as possible. Following
the tips above will help ensure your changes are reviewed. the tips above will help ensure your changes are reviewed.
## Testing contribution pull requests <!-- ## Testing contribution pull requests
OHIF uses [netlify](https://www.netlify.com/) so that pull requests are OHIF uses [netlify](https://www.netlify.com/) so that pull requests are
autogenerated and available for testing. autogenerated and available for testing.
@ -133,7 +140,7 @@ the gitbook generated by the pull request:
```bash ```bash
https://deploy-preview-237--ohif.netlify.com/contributing.html https://deploy-preview-237--ohif.netlify.com/contributing.html
``` ``` -->
<!-- <!--
Links Links
@ -143,4 +150,5 @@ https://deploy-preview-237--ohif.netlify.com/contributing.html
[example-url]: https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json [example-url]: https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json
[pr-237]: https://github.com/OHIF/Viewers/pull/237 [pr-237]: https://github.com/OHIF/Viewers/pull/237
[monorepo]: https://github.com/OHIF/Viewers/issues/768 [monorepo]: https://github.com/OHIF/Viewers/issues/768
[unlink]: https://stackoverflow.com/questions/58459698/is-there-a-command-to-unlink-all-yarn-packages-yarn-unlink-all
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -47,6 +47,9 @@ Navigate to the root of the project's directory in your terminal and run the
following commands: following commands:
```bash ```bash
# Switch to the v3 branch
git switch feat/v2-main
# Restore dependencies # Restore dependencies
yarn install yarn install
@ -67,10 +70,7 @@ You should see the following output:
### 🎉 Celebrate 🎉 ### 🎉 Celebrate 🎉
<center> <div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/545988245?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="getting-started"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
<img alt="development server hosted app" src="/assets/img/loading-study.gif" />
<i>Our app, hosted by the development server</i>
</center>
### Building for Production ### Building for Production

View File

@ -1,7 +1,72 @@
# Contributing: Tests # Running Tests for OHIF
We introduce here various test types that is available
for OIHF, and how to run each test in order to make sure your contribution
hasn't broken any existing functionalities. Idea and philosophy of each testing category is discussed in the second
part of this page.
## Unit test
To run the unit test:
```
yarn run test:unit:ci
```
Note: You should have already installed all the packages with `yarn install`.
Running unit test will generate a report at the end showing the successful and
unsuccessful tests with detailed explanations.
## End-to-end test
For running the OHIF e2e test you need to run the following steps:
- Create a mini-pacs for OHIF to access the images for testing. We download
and run our lightweight implementation which provides a collection of DICOM studies ([source code][mini-pacs]).
```
docker run -p 5985:5985 -p 5984:5984 -e USE_POUCHDB=true -e DB_SERVER=http://0.0.0.0 ohif/viewer-testdata:0.1-test
```
Successful execution should be
![](../assets/img/docker-pacs.png)
- Open a new terminal, navigate to the OHIF project, and run OHIF with the dicom-server config
```
APP_CONFIG=config/dicomweb-server.js yarn start
```
You should be able to see test studies in the study list
![OHIF-e2e-test-studies](../assets/img/OHIF-e2e-test-studies.png)
- Open a new terminal inside the OIHF project, and run the e2e cypress test
```
yarn run test:e2e
```
You should be able to see the cypress window open
![e2e-cypress](../assets/img/e2e-cypress.png)
Run the tests by clicking on the `Run #number integration tests` .
A new window will open and you will see e2e tests being executed one after
each other.
![e2e-cypress-final](../assets/img/e2e-cypress-final.png)
# Testing Philosiphy
> Testing is an opinionated topic. Here is a rough overview of our testing > Testing is an opinionated topic. Here is a rough overview of our testing
> philosiphy. See something you want to discuss or think should be changed? Open > philosophy. See something you want to discuss or think should be changed? Open
> a PR and let's discuss. > a PR and let's discuss.
You're an engineer. You know how to write code, and writing tests isn't all that You're an engineer. You know how to write code, and writing tests isn't all that
@ -128,6 +193,7 @@ feature `X` or scenario `Y`? Open an issue and let's discuss.
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[eslint-rules]: https://eslint.org/docs/rules/ [eslint-rules]: https://eslint.org/docs/rules/
[mini-pacs]: https://github.com/OHIF/viewer-testdata
[typescript-docs]: https://www.typescriptlang.org/docs/home.html [typescript-docs]: https://www.typescriptlang.org/docs/home.html
[flow-org]: https://flow.org/ [flow-org]: https://flow.org/
<!-- Talks --> <!-- Talks -->

View File

@ -7,6 +7,19 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- CORNERSTONE.js -->
<tr>
<td>
<a href="">
Default
</a>
</td>
<td>
Default extension provides default viewer layout, a study/series
browser, and a datasource that maps to a DICOMWeb compliant backend
</td>
<td>commandsModule, ContextModule, DataSourceModule, HangingProtocolModule, LayoutTemplateModule, PanelModule, SOPClassHandlerModule, ToolbarModule</td>
</tr>
<!-- CORNERSTONE.js --> <!-- CORNERSTONE.js -->
<tr> <tr>
<td> <td>
@ -15,34 +28,12 @@
</a> </a>
</td> </td>
<td> <td>
A viewport powered by <code>cornerstone.js</code>. Adds support for 2D DICOM rendering and manipulation, as well as support for the tools features in <a href="https://tools.cornerstonejs.org/examples/"><code>cornerstone-tools</code></a>. Also adds "CINE Dialog" to the Toolbar. Provides rendering functionalities for 2D images.
</td> </td>
<td>Viewport, Toolbar</td> <td>ViewportModule, CommandsModule</td>
</tr>
<!-- VTK.js -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-vtk">
VTK.js
</a>
</td>
<td>
A viewport powered by <code>vtk.js</code>. Adds support for volume renderings and advanced features like MPR. Also adds "3D Rotate" to the Toolbar.
</td>
<td>Viewport, Toolbar</td>
</tr>
<!-- dicom-html -->
<tr>
<td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
</td>
<td>
Renders text and HTML content for <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12">specific SopClassUIDs</a>.
</td>
<td>Viewport, SopClassHandler</td>
</tr> </tr>
<!-- dicom-pdf --> <!-- dicom-pdf -->
<tr> <!-- <tr>
<td> <td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a> <a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
</td> </td>
@ -50,26 +41,27 @@
Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>. Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>.
</td> </td>
<td>Viewport, SopClassHandler</td> <td>Viewport, SopClassHandler</td>
</tr> </tr> -->
<!-- dicom-microscopy --> <!-- dicom-sr -->
<tr> <tr>
<td> <td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a> <a href="">DICOM SR</a>
</td> </td>
<td> <td>
Renders Microscopy images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7">specific SopClassUID</a>. Maintained extensions for cornerstone and visualization of DICOM Structured Reports
</td> </td>
<td>Viewport, SopClassHandler</td> <td>ViewportModule, CommandsModule, SOPClassHandlerModule</td>
</tr> </tr>
<!-- dicom-segmentation --> <!-- Measurement-tracking -->
<tr> <tr>
<td> <td>
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-segmentation">DICOM Segmentation</a> <a href="">Measurement tracking</a>
</td> </td>
<td> <td>
Renders segmentation images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7">specific SopClassUID</a>. Tracking measurements in the measurement panel
</td> </td>
<td>Panel, Toolbar</td> <td> ContextModule,PanelModule,ViewportModule,CommandsModule</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -0,0 +1,94 @@
<table>
<thead>
<tr>
<th align="left" width="30%">
Types
</th>
<th align="left">Description</th>
<th align="left">Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">
<a href="./modules/layout-template.md">
LayoutTemplate (NEW)
</a>
</td>
<td align="left">Control Layout of a route</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/data-source.md">
DataSource (NEW)
</a>
</td>
<td align="left">Control the mapping from DICOM metadata to OHIF-metadata</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/sop-class-handler.md">
SOPClassHandler
</a>
</td>
<td align="left">Determines how retrieved study data is split into "DisplaySets"</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/panel.md">
Panel
</a>
</td>
<td align="left">Adds left or right hand side panels</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/viewport.md">
Viewport
</a>
</td>
<td align="left">Adds a component responsible for rendering a "DisplaySet"</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/commands.md">
Commands
</a>
</td>
<td align="left">Adds named commands, scoped to a context, to the CommandsManager</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/toolbar.md">
Toolbar
</a>
</td>
<td align="left">Adds buttons or custom components to the toolbar</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/context.md">
Context
</a>
</td>
<td align="left">Shared state for a workflow or set of extension module definitions</td>
<td align="left"></td>
</tr>
<tr>
<td align="left">
<a href="./modules/hpModule.md">
HangingProtocol
</a>
</td>
<td align="left">Adds hanging protocol rules</td>
<td align="left"></td>
</tr>
</tbody>
</table>

View File

@ -1,53 +1,50 @@
# Extensions # Extensions
- [Extensions](#extensions)
- [Overview](#overview) - [Overview](#overview)
- [Concepts](#concepts)
- [Extension Skeleton](#extension-skeleton) - [Extension Skeleton](#extension-skeleton)
- [OHIF-Maintained Extensions](#ohif-maintained-extensions)
- [Registering an Extension](#registering-an-extension) - [Registering an Extension](#registering-an-extension)
- [Lifecylce Hooks](#lifecycle-hooks) - [Registering at Runtime](#registering-at-runtime)
- [Registering at Build Time](#registering-at-build-time)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Modules](#modules) - [Modules](#modules)
- [Contexts](#contexts) - [Contexts](#contexts)
- [Consuming Extensions](#consuming-extensions)
- [Extension Manager](#extensionmanager)
- [Maintained Extensions](#maintained-extensions)
## Overview ## Overview
We have re-designed the architecture of the `OHIF-v3` to enable building applications
that are easily extensible to various use cases (modes) that behind the scene would utilize desired functionalities (extensions) to reach the goal of the use case.
We use extensions to help us isolate and package groups of related features. Previously, extensions were “additive” and could not easily be mixed and matched within the same viewer for different use cases. Previous `OHIF-v2` architecture meant that
Extensions provide functionality, ui components, and new behaviors. Ideally, any minor extension alteration usually would require the user to hard fork. E.g. removing some of the tools from the toolbar of the cornerstone extension meant you had to hard fork it, which was frustrating if the implementation was otherwise the same as master.
they're built in a way that allows them to extend entirely different
implementations of the `@ohif/viewer` project.
> - Developers should make packages of *reusable* functionality as extensions, and can consume
> publicly available extensions.
> - Any conceivable radiological workflow or viewer setup will be able to be built with the platform through *modes*.
Practical examples of extensions include:
- A set of segmentation tools that build on top of the `cornerstone` viewport
- A set of rendering functionalities to volume render the data
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
<mark>Diagram showing how extensions are configured and accessed.
<!--
<div style="text-align: center;"> <div style="text-align: center;">
<a href="/assets/img/extensions-diagram.png"> <a href="/assets/img/extensions-diagram.png">
<img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" /> <img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" />
</a> </a>
<div><i>Diagram showing how extensions are configured and accessed.</i></div> <div><i>Diagram showing how extensions are configured and accessed.</i></div>
</div> </div> -->
The `@ohif/viewer`'s application level configuration gives us the ability to add
and configure extensions. When the application starts, extensions are registered
with the `ExtensionManager`. Different portions of the `@ohif/viewer` project
will use registered extensions to influence application behavior.
Extensions allow us to:
- Wrap and integrate functionality of 3rd party dependencies in a reusable way ## Extension Skeleton
- Change how application data is mapped and transformed
- Display a consistent/cohesive UI
- Inject custom components to override built-in components
Practical examples of extensions include:
- A set of segmentation tools that build on top of the `cornerstone` viewport
- Showing ML/AI report summaries for the selected study/series/image
- Support for parsing DICOM structured reports and displaying them in a user
friendly way
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
## Concepts
### Extension Skeleton
An extension is a plain JavaScript object that has an `id` property, and one or An extension is a plain JavaScript object that has an `id` property, and one or
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks). more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
@ -62,45 +59,77 @@ export default {
// Lifecyle // Lifecyle
preRegistration() { /* */ }, preRegistration() { /* */ },
onModeEnter() { /* */ },
onModeExit() { /* */ },
// Modules // Modules
getCommandsModule() { /* */ }, getLayoutTemplateModule() { /* */ },
getToolbarModule() { /* */ }, getDataSourcesModule() { /* */ },
getSopClassHandlerModule() { /* */ },
getPanelModule() { /* */ }, getPanelModule() { /* */ },
getSopClassHandler() { /* */ },
getViewportModule() { /* */ }, getViewportModule() { /* */ },
getCommandsModule() { /* */ },
getContextModule() { /* */ },
getToolbarModule() { /* */ },
getHangingProtocolModule() { /* */ },
} }
``` ```
### Registering an Extension ## OHIF-Maintained Extensions
A small number of powerful extensions for popular use cases are maintained by
OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in
the top level [`extensions/`][ext-source] directory.
There are two different ways to register and configure extensions: At {% include "./_maintained-extensions-table.md" %}
## Registering an Extension
Extensions are building blocks that need to be registered. There are two different ways to register and configure extensions: At
[runtime](#registering-at-runtime) and at [runtime](#registering-at-runtime) and at
[build time](#registering-at-build-time). [build time](#registering-at-build-time).
You can leverage one or both strategies. Which one(s) you choose depend on your 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 application's requirements.
becomes available to the core application via the `ExtensionManager`.
#### Registering at Runtime Each [module](#modules) defined by the extension
becomes available to the modes via the `ExtensionManager` by requesting it via
its id. [Read more about Extension Manager](#extension-manager)
### Registering at Runtime
The `@ohif/viewer` uses a [configuration file](../viewer/configuration.md) at The `@ohif/viewer` uses a [configuration file](../viewer/configuration.md) at
startup. The schema for that file includes an `Extensions` key that supports an startup. The schema for that file includes an `extensions` key that supports an
array of extensions to register. array of extensions to register.
```js ```js
import MyFirstExtension from '@ohif/extension-first'
import MySecondExtension from '@ohif/extension-second'
const extensionConfig = {/* extension configuration */}
// prettier-ignore // prettier-ignore
const config = { const config = {
routerBasename: '/',
extensions: [ extensions: [
MyFirstExtension, MyFirstExtension,
[ [
MySecondExtension, MySecondExtension,
{ /* MySecondExtensions Configuration */ }, extensionConfig
], ],
]; ],
modes: [/* modes */],
showStudyList: true,
dataSources: [ /* data source config */]
} }
``` ```
#### Registering at Build Time Then, behind the scene, the runtime-added extensions will get merged with the
default app extensions (note: default app extensions include: `OHIFDefaultExtension`,
`OHIFCornerstoneExtension`, `OHIFDICOMSRExtension`,
`OHIFMeasurementTrackingExtension`)
### Registering at Build Time
The `@ohif/viewer` works best when built as a "Progressive Web Application" 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 (PWA). If you know the extensions your application will need, you can specify
@ -114,38 +143,63 @@ You can update the list of bundled extensions by:
1. Having your `@ohif/viewer` project depend on the extension 1. Having your `@ohif/viewer` project depend on the extension
2. Importing and adding it to the list of extensions in the 2. Importing and adding it to the list of extensions in the
`<repo-root>/platform/src/index.js` entrypoint. `<repo-root>/platform/src/index.js` entrypoint:
```js
import OHIFDefaultExtension from '@ohif/extension-default';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr';
import MyFirstExtension from '@ohif/extension-first'
/** Combine our appConfiguration and "baked-in" extensions */
const appProps = {
config: window ? window.config : {},
defaultExtensions: [
OHIFDefaultExtension,
OHIFCornerstoneExtension,
OHIFMeasurementTrackingExtension,
OHIFDICOMSRExtension,
MyFirstExtension
],
};
```
## Lifecycle Hooks
Currently, there are three lifecycle hook for extensions:
### Lifecycle Hooks
Currently, there is only a single lifecycle hook for extensions:
[`preRegistration`](./lifecycle/pre-registration.md) [`preRegistration`](./lifecycle/pre-registration.md)
This hook is called once on initialization of the entire viewer application, used to initialize the extensions state, and consume user defined extension configuration. If an extension defines the [`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 lifecycle hook, it is called before any modules are registered in the
`ExtensionManager`. It's most commonly used to wire up extensions to `ExtensionManager`. It's most commonly used to wire up extensions to
[services](./../services/index.md) and [commands](./modules/commands.md), and to [services](./../services/index.md) and [commands](./modules/commands.md), and to
bootstrap 3rd party libraries. bootstrap 3rd party libraries.
### Modules
Modules are the meat of extensions. They provide "definitions", components, and [`onModeEnter`](./lifecycle/on-mode-enter.md): This hook is called whenever a new mode is entered, or a modes data or datasource is switched. This hook can be used to initialize data.
filtering/mapping logic that are then made available by various managers and
services. [`onModeExit`](./lifecycle/on-mode-exit.md): Similarly to onModeEnter, this hook is called when navigating away from a mode, or before a modes data or datasource is changed. This can be used to clean up data (e.g. remove annotations that do not need to be persisted)
## Modules
Modules are the meat of extensions, the `blocks` that we have been talking about a lot.
They provide "definitions", components, and filtering/mapping logic that are then made available to modes and services.
Each module type has a special purpose, and is consumed by our viewer Each module type has a special purpose, and is consumed by our viewer
differently. differently.
| Type | Description | Examples |
| ------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------- | {% include "./_modules.md" %}
| [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> <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 ### Contexts
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
@ -163,6 +217,8 @@ 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" to the appropriate context. When the `command` is fired, the "active contexts"
are used to determine the appropriate implementation of the rotate behavior. are used to determine the appropriate implementation of the rotate behavior.
<!-- <mark>do we want the followings?
## Consuming Extensions ## Consuming Extensions
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer` We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
@ -189,45 +245,12 @@ applications requiring a high degree of customization that can't be achieved
with current theming, configuration, extension, and services support. with current theming, configuration, extension, and services support.
If you're not sure how to achieve your goals with the extensibility available If you're not sure how to achieve your goals with the extensibility available
today, create a GitHub issue! 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` along with the
application's configuration through the appConfig key (optional).
```js
const commandsManager = new CommandsManager();
const servicesManager = new ServicesManager();
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
appConfig,
});
```
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 the extension's
config, the application's config and `ExtensionManager`'s `ServicesManager` and
`CommandsManager` instances.
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
appropriate places in our application.
## Maintained Extensions
A small number of powerful extensions for popular use cases are maintained by
OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in
the top level [`extensions/`][ext-source] directory.
{% include "./_maintained-extensions-table.md" %}
<!-- <!--
LINKS LINKS

View File

@ -0,0 +1,29 @@
# Lifecycle Hook: onModeEnter
If an extension defines the `onModeEnter` lifecycle hook, it is called
when a new mode is enters, or a mode's data or datasource is switched.
For instance, in DICOM structured report extension (`dicom-sr`), we are using
`onModeEnter` to re-create the displaySets after a new mode is entered.
_Example `onModeEnter` hook implementation_
```js
export default {
id: 'org.ohif.dicom-sr',
onModeEnter({ servicesManager}) {
const { DisplaySetService } = servicesManager.services;
const displaySetCache = DisplaySetService.getDisplaySetCache();
const srDisplaySets = displaySetCache.filter(
ds => ds.SOPClassHandlerId === SOPClassHandlerId
);
srDisplaySets.forEach(ds => {
// New mode route, allow SRs to be hydrated again
ds.isHydrated = false;
});
},
};
```

View File

@ -0,0 +1,17 @@
# Lifecylce Hook: onModeExit
If an extension defines the `onModeExit` lifecycle hook, it is called when navigating
away from a mode. This hook can be used to clean up data tasks such as unregistering services,
removing annotations that do not need to be persisted.
_Example `onModeExit` hook implementation_
```js
export default {
id: 'myExampleExtension',
onModeExit({ servicesManager, commandsManager}) {
myCacheServie.purge()
},
};
```

View File

@ -13,9 +13,30 @@ The `preRegistration` hook receives an object containing the
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any `ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
`configuration` that was provided with the extension at time of registration. `configuration` that was provided with the extension at time of registration.
_Example `preRegistration` hook implementation_ Example `preRegistration` implementation that register a new service and make it
available in the app. We will talk more in details for creating a new service for
`OHIF-v3`.
```js ```js
// new service inside new extension
import MyNewService from './MyNewService';
export default function MyNewServiceWithServices(serviceManager) {
return {
name: 'MyNewService',
create: ({ configuration = {} }) => {
return new MyNewService(serviceManager);
},
};
}
```
and
```js
import MyNewService from './MyNewService'
export default { export default {
id: 'MyExampleExtension', id: 'MyExampleExtension',
@ -35,6 +56,10 @@ export default {
console.log('Important stuff has been wired.'); console.log('Important stuff has been wired.');
window.importantStuff(); window.importantStuff();
// Registering new services
servicesManager.registerService(MyNewService(servicesManager));
},
}, },
}; };
``` ```

View File

@ -1,13 +1,18 @@
# Module: Commands # Module: Commands
- [Module: Commands](#module-commands)
- [Overview](#overview) - [Overview](#overview)
- [Command Definitions](#command-definitions) - [Command Definitions](#command-definitions)
- [Commands Manager](#commands-manager) - [Command Behavior](#command-behavior)
- [Instantiating](#instatiating) - [`CommandsManager` Public API](#commandsmanager-public-api)
- [Public API](#public-api)
- [Contexts](#contexts) - [Contexts](#contexts)
## Overview ## Overview
`CommandsModule` includes list of arbitrary functions. These may activate tools, communicate with a server, open a modal, etc.
The significant difference between `OHIF-v3` and `OHIF-v2` is that in `v3` a `mode` defines
its toolbar, and which commands each tool call is inside in its toolDefinition
An extension can register a Commands Module by defining a `getCommandsModule` 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 method. The Commands Module allows us to register one or more commands scoped to
@ -17,69 +22,55 @@ characteristics that make them tremendously powerful:
- Multiple implementations for the same command can be defined - Multiple implementations for the same command can be defined
- Only the correct command's implementation will be run, dependent on the - Only the correct command's implementation will be run, dependent on the
application's "context" application's "context"
- Commands can be called from extensions, modules, and the consuming application - Commands are used by hotkeys, toolbar buttons and render settings
Here is a simple example commands module: Here is a simple example commands module:
```js ```js
export default { const getCommandsModule = () => ({
id: 'example-commands-module',
/**
* @param {object} params
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
*/
getCommandsModule({ servicesManager, commandsManager }) {
return {
definitions: { definitions: {
sayHello: { exampleActionDef: {
commandFn: ({ words }) => { commandFn: ({ param1 }) => {
console.log(words); console.log(`param1's value is: ${param1}`);
}, },
options: { words: 'Hello!' }, // storeContexts: ['viewports'],
options: { param1: 'param1' },
context: 'VIEWER', // optional
}, },
}, },
defaultContext: 'VIEWER', defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
}; });
},
};
``` ```
Each definition returned by the Commands Module is registered to the Each definition returned by the Commands Module is registered to the
`ExtensionManager`'s `CommandsManager`. `ExtensionManager`'s `CommandsManager`.
> `storeContexts` has been removed in `OHIF-v3` and now modules have access to all commands and services. This change enables support for user-registered services.
## Command Definitions ## Command Definitions
The command definition consists of a named command (`myCommandName` below) and a The command definition consists of a named command (`exampleActionDef` below) and a
`commandFn`. The command name is used to call the command, and the `commandFn` `commandFn`. The command name is used to call the command, and the `commandFn`
is the "command" that is actioned. is the "command" that is actioned. T
```js ```js
myCommandName: { exampleActionDef: {
commandFn: ({ viewports, other, options }) => { }, commandFn: ({ param1, options }) => { },
storeContexts: ['viewports'], options: { param1: 'measurement' },
options: { words: 'Just kidding! Goodbye!' }, context: 'DEFAULT',
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
} }
``` ```
| Property | Type | Description | | Property | Type | Description |
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. | | `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` | | `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. | | `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
## Command Behavior ## 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** **If there are multiple valid commands for the application's active contexts**
@ -93,45 +84,21 @@ active.
- When to use: a `hotkey` (like "invert") that doesn't make sense for the - When to use: a `hotkey` (like "invert") that doesn't make sense for the
current viewport (PDF or HTML) current viewport (PDF or HTML)
## `CommandsManager` ## `CommandsManager` Public API
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 If you would like to run a command in the consuming app or an extension, you can
use one of the following methods: use `CommandsManager.runCommand(commandName, options = {}, contextName)`
```js ```js
// Returns all commands for a given context // Returns all commands for a given context
commandsManager.getContext('string'); commandsManager.getContext('string');
// Attempts to run a command // Run a command, it will run all the `speak` commands in all contexts
commandsManager.runCommand('speak', { command: 'hello' }); commandsManager.runCommand('speak', { command: 'hello' });
// Run command, but override the active contexts // Run command, from Default context
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']); commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']);
``` ```
The `ExtensionManager` handles registering commands and creating contexts, so The `ExtensionManager` handles registering commands and creating contexts, so
@ -152,7 +119,7 @@ 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 which ones are currently active. As extensions depend heavily on these, we will
likely publish guidance around creating contexts, and ways to override extension likely publish guidance around creating contexts, and ways to override extension
defined contexts in the near future. If you would like to discuss potential 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 changes to how contexts work, please don't hesitate to create a new GitHub
issue. issue.
[Some additional information on Contexts can be found here.](./../index.md#contexts) [Some additional information on Contexts can be found here.](./../index.md#contexts)

View File

@ -0,0 +1,30 @@
# Module: Context
- [Module: Context](#module-context)
- [Overview](#overview)
## Overview
This new module type allows you to connect components via a shared context. You can create a context that two components, e.g. a viewport and a panel can use to synchronize and communicate. An extensive example of this can be seen in the longitudinal modes custom extensions.
```jsx
const ExampleContext = React.createContext();
function ExampleContextProvider({ children }) {
return (
<ExampleContext.Provider value={{ example: 'value' }}>
{children}
</ExampleContext.Provider>
);
}
const getContextModule = () => [
{
name: 'ExampleContext',
context: ExampleContext,
provider: ExampleContextProvider,
},
];
```

View File

@ -0,0 +1,85 @@
# Module: Data Source
## Overview
The internal data structure of OHIFs metadata follows naturalized DICOM JSON, A format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords instead of tags and sequences as arrays, for easy development and clear code.
We have built a standard for fetching and mapping data into OHIFs native format, which we call DataSources, and have provided one implementation of this standard.
You can make another datasource implementation which communicates to your backend and maps to OHIFs native format, then use any existing mode on your platform. Your data doesnt even need to be DICOM if you can map some proprietary data to the correct format.
The DataSource is also a place to add easy helper methods that platform-specific extensions can call in order to interact with the backend, meaning proprietary data interactions can be wrapped in extensions.
```js
const getDataSourcesModule = () => [
{
name: 'exampleDataSource',
type: 'webApi', // 'webApi' | 'local' | 'other'
createDataSource: dataSourceConfig => {
return IWebApiDataSource.create(/* */);
},
},
];
```
Default extension provides two main data sources that are commonly used: `dicomweb` and `dicomjson`
```js
import { createDicomWebApi } from './DicomWebDataSource/index.js';
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
function getDataSourcesModule() {
return [
{
name: 'dicomweb',
type: 'webApi',
createDataSource: createDicomWebApi,
},
{
name: 'dicomjson',
type: 'jsonApi',
createDataSource: createDicomJSONApi,
},
];
}
```
## Custom DataSource
You can add your custom datasource by creating the implementation using `IWebApiDataSource.create` from `@ohif/core`. This factory function creates a new "Web API" data source that fetches data over HTTP.
You need to make sure, you implement the following functions for the data source.
```js
// platform/core/src/DataSources/IWebApiDataSource.js
function create({
query,
retrieve,
store,
reject,
parseRouteParams,
retrieveSeriesMetadata,
deleteStudyMetadataPromise,
getImageIdsForDisplaySet,
getImageIdsForInstance,
}) {
/* */
}
```
You can take a look at `dicomweb` data source implementation to get an idea
`extensions/default/src/DicomWebDataSource/index.js`
## DicomMetadataStore
In `OHIF-v3` we have a central location for the metadata of studies and they are located
in `DicomMetadataStore`. Your custom datasource can communicate with `DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will learn more about `DicomMetadataStore` in services.

View File

@ -0,0 +1,52 @@
Module: Hanging Protocol
## Overview
`hangingProtocolModule` provides the protocols for hanging the displaySets in the viewer.
This module can be as simple as loading a list of pre-defined protocols, or it can be more complex
and `fetch` the protocols from a server.
```js
const deafultProtocol = {
id: 'defaultProtocol',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'Default',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2021-02-23T19:22:08.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
stages: [
{
id: 'nwzau7jDkEkL8djfr',
name: 'oneByOne',
viewportStructure: {
type: 'grid',
properties: {
rows: 1,
columns: 1,
},
},
viewports: [
{
viewportSettings: [],
imageMatchingRules: [],
seriesMatchingRules: [],
studyMatchingRules: [],
},
],
createdDate: '2021-02-23T19:22:08.894Z',
},
],
numberOfPriorsReferenced: -1,
};
function getHangingProtocolModule() {
return [
{
name: hangingProtocolName,
protocols: [deafultProtocol],
},
];
}
```

View File

@ -0,0 +1,136 @@
# Module: Layout Template
- [Module: Layout Template](#module-layout-template)
- [Overview](#overview)
- [Overview Video](#overview-video)
## Overview
`LayoutTemplates` are a new concept in v3 that modes use to control the layout of a route.
A layout template is a React component that is given a set of managers that define apis to access toolbar state, commands, and hotkeys, as well as props defined by the layout template.
For instance the default LayoutTemplate takes in leftPanels, rightPanels and viewports as props, which it uses to build its view.
In addition, `layout template` has complete control over the structure of the application. You could have tools down the left side, or a strict guided workflow with tools set programmatically, the choice is yours for your use case.
```jsx
const getLayoutTemplateModule = (/* ... */) => [
{
id: 'exampleLayout',
name: 'exampleLayout',
component: ExampleLayoutComponent,
},
];
```
The `props` that are passed to `layoutTemplate` are managers and service, along with
the defined mode left/right panels, mode's defined viewports and OHIF `ViewportGridComp`.
LayoutTemplate leverages extensionManager to grab typed extension module entries:
`*.getModuleEntry(id)`
A simplified code for `Default extention`'s layout template is:
```jsx
// extensions/default/src/ViewerLayout/index.jsx
import React from 'react'
import { SidePanel } from '@ohif/ui'
function Toolbar({ servicesManager }) {
const { ToolBarService } = servicesManager.services
return (
<>
// ToolBarService.getButtonSection('primary') to get toolbarButtons
{toolbarButtons.map((toolDef, index) => {
const { id, Component, componentProps } = toolDef
return (
<Component
key={id}
id={id}
{...componentProps}
bState={buttonState}
isActive={isActive}
onInteraction={(args) => ToolBarService.recordInteraction(args)}
/>
)
})}
</>
)
}
function ViewerLayout({
// From Extension Module Params
extensionManager,
servicesManager,
hotkeysManager,
commandsManager,
// From Modes
leftPanels,
rightPanels,
viewports,
ViewportGridComp,
}) {
const getPanelData = (id) => {
const entry = extensionManager.getModuleEntry(id)
const content = entry.component
return {
iconName: entry.iconName,
iconLabel: entry.iconLabel,
label: entry.label,
name: entry.name,
content,
}
}
const getViewportComponentData = (viewportComponent) => {
const entry = extensionManager.getModuleEntry(viewportComponent.namespace)
return {
component: entry.component,
displaySetsToDisplay: viewportComponent.displaySetsToDisplay,
}
}
const leftPanelComponents = leftPanels.map(getPanelData)
const rightPanelComponents = rightPanels.map(getPanelData)
const viewportComponents = viewports.map(getViewportComponentData)
return (
<div>
<Toolbar servicesManager={servicesManager} />
<div>
{/* LEFT SIDEPANELS */}
<SidePanel
side="left"
defaultComponentOpen={leftPanelComponents[0].name}
childComponents={leftPanelComponents}
/>
{/* TOOLBAR + GRID */}
<ViewportGridComp
servicesManager={servicesManager}
viewportComponents={viewportComponents}
commandsManager={commandsManager}
/>
{/* Rigth SIDEPANELS */}
<SidePanel
side="right"
defaultComponentOpen={rightPanelComponents[0].name}
childComponents={rightPanelComponents}
/>
</div>
</div>
)
}
```
## Overview Video
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/545993263?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Viewer-layout"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>

View File

@ -1,13 +1,16 @@
# Module: Panel # Module: Panel
## Overview
UI that is intended to be displayed within a panel. The default LayoutTemplate has panels on the left and right sides, however one could make a template with panels at the top or bottom and make extensions with panels intended for such slots.
An extension can register a Panel Module by defining a `getPanelModule` method. An extension can register a Panel Module by defining a `getPanelModule` method.
The panel module provides the ability to define `menuOptions` and `components` 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 used by the consuming application. `components` are React Components
that can be displayed in the consuming application's "Panel" Component. 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> <!-- <center><i>A panel extension example</i></center> -->
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can `defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
@ -16,46 +19,83 @@ optional provide it's own `context` value.
The `getPanelModule` receives an object containing the `ExtensionManager`'s The `getPanelModule` receives an object containing the `ExtensionManager`'s
associated `ServicesManager` and `CommandsManager`. associated `ServicesManager` and `CommandsManager`.
![panel-module-v3](../../assets/img/panel-module-v3.png)
```js ```js
import MyComponent from './MyComponent.js'; import PanelMeasurementTable from './PanelMeasurementTable.js';
export default {
id: 'example-panel-module',
/** function getPanelModule({
* @param {object} params commandsManager,
* @param {ServicesManager} params.servicesManager extensionManager,
* @param {CommandsManager} params.commandsManager servicesManager,
*/ }) {
getPanelModule({ servicesManager, commandsManager }) { const wrappedMeasurementPanel = () => {
return { return (
menuOptions: [ <PanelMeasurementTable
{ commandsManager={commandsManager}
// A suggested icon servicesManager={servicesManager}
// 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',
// UI Hint; If the target panel is in a "disabled" state
isDisabled: studies => {
return false;
},
// Overrides `defaultContext`, if specified
context: ['ACTIVE_VIEWPORT:MAGIC'],
},
],
components: [
{
id: 'target-component-id',
component: MyComponent,
},
],
defaultContext: ['ROUTE:VIEWER'],
}; };
return [
{
name: 'measure',
iconName: 'list-bullets',
iconLabel: 'Measure',
label: 'Measurements',
isDisabled: studies => {}, // optional
component: wrappedMeasurementPanel,
}, },
}; ];
}
```
## Consuming Panels Inside Modes
As explained earlier, extensions make the functionalities and components available and
`modes` utilize them to build an app. So, as seen above, we are not actually defining
which side the panel should be opened. Our extension is providing the component with
its.
New: You can easily add multiple panels to the left/right side of the viewer using the mode
configuration. As seen below, the `leftPanels` and `rightPanels` accept an `Array` of
the `IDs`.
```js
export default function mode({ modeConfiguration }) {
return {
id: "viewer",
routes: [
{
path: "longitudinal",
layoutTemplate: ({ location, servicesManager }) => {
return {
id,
props: {
leftPanels: [
"org.ohif.measurement-tracking.panelModule.seriesList",
],
rightPanels: [
"org.ohif.measurement-tracking.panelModule.trackedMeasurements",
],
viewports,
},
};
},
},
],
extensions: [
"org.ohif.default",
"org.ohif.cornerstone",
"org.ohif.measurement-tracking",
"org.ohif.dicom-sr",
],
};
}
``` ```

View File

@ -1,8 +1,13 @@
# Module: SOP Class Handler # Module: SOP Class Handler
An extension can register a [SOP Class][sop-class-link] Handler Module by
defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link] ## Overview
Handler is a bit different from the other modules, as it doesn't provide a `1:1` This module defines how a specific DICOM SOP class should be processed to make a displaySet, something that can be hung in a viewport. An extension can register a [SOP Class][sop-class-link] Handler Module by defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link].
The mode chooses what SOPClassHandlers to use, so you could process a series in a different way depending on mode within the same application.
SOPClassHandler 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: schema for UI or provide it's own components. It instead defines:
- `sopClassUIDs`: an array of string SOP Class UIDs that the - `sopClassUIDs`: an array of string SOP Class UIDs that the
@ -14,87 +19,82 @@ A `displaySet` has the following shape:
```js ```js
return { return {
plugin: 'html', Modality: 'MR',
Modality: 'SR', displaySetInstanceUIDD
displaySetInstanceUID: 0, SeriesDate,
wadoRoot: study.getData().wadoRoot, SeriesTime,
wadoUri: instance.getData().wadouri, SeriesInstanceUID,
SOPInstanceUID: instance.getSOPInstanceUID(), StudyInstanceUID,
SeriesInstanceUID: series.getSeriesInstanceUID(), SeriesNumber,
StudyInstanceUID: study.getStudyInstanceUID(), FrameRate,
authorizationHeaders, SeriesDescription,
}; isMultiFrame,
numImageFrames,
SOPClassHandlerId,
}
``` ```
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 ## Example SOP Class Handler Module
```js ```js
const SOP_CLASS_UIDS = { import ImageSet from '@ohif/core/src/classes/ImageSet';
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',
const sopClassDictionary = {
CTImageStorage: "1.2.840.10008.5.1.4.1.1.2",
MRImageStorage: "1.2.840.10008.5.1.4.1.1.4",
}; };
export default {
id: 'example-sop-class-handler-module',
/** // It is important to note that the used SOPClassUIDs in the modes are in the order that is specified in the array.
* @param {object} params const sopClassUids = [
* @param {ServicesManager} params.servicesManager sopClassDictionary.CTImageStorage,
* @param {CommandsManager} params.commandsManager sopClassDictionary.MRImageStorage,
*/ ;
getSopClassHandlerModule({ servicesManager, commandsManager }) {
return {
id: 'OHIFDicomHtmlSopClassHandler',
sopClassUIDs: Object.values(SOP_CLASS_UIDS),
/** const makeDisplaySet = (instances) => {
* @param {object} series - const instance = instances[0];
* @param {object} study - const imageSet = new ImageSet(instances);
* @param {object} dicomWebClient -
* @param {object} authorizationHeaders -
*/
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
const instance = series.getFirstInstance();
return { imageSet.setAttributes({
plugin: 'html', displaySetInstanceUID: imageSet.uid,
displaySetInstanceUID: 0, SeriesDate: instance.SeriesDate,
wadoRoot: study.getData().wadoRoot, SeriesTime: instance.SeriesTime,
wadoUri: instance.getData().wadouri, SeriesInstanceUID: instance.SeriesInstanceUID,
SOPInstanceUID: instance.getSOPInstanceUID(), StudyInstanceUID: instance.StudyInstanceUID,
SeriesInstanceUID: series.getSeriesInstanceUID(), SeriesNumber: instance.SeriesNumber,
StudyInstanceUID: study.getStudyInstanceUID(), FrameRate: instance.FrameTime,
authorizationHeaders, SeriesDescription: instance.SeriesDescription,
Modality: instance.Modality,
isMultiFrame: isMultiFrame(instance),
numImageFrames: instances.length,
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
});
return imageSet;
}; };
getSopClassHandlerModule = () => {
return [
{
name: 'stack,
sopClassUids,
getDisplaySetsFromSeries: makeDisplaySet,
}, },
} ];
}; };
``` ```
### More examples : ### More examples :
You can find another example for this mapping between raw metadata and displaySet for
- [Dicom-HTML SOP][dicom-html-sop] `DICOM-SR` extension.
- [Dicom-PDF SOP][dicom-pdf-sop]
- [Dicom-Microscopy SOP][dicom-micro-sop]
- [Dicom-Segmentation SOP][dicom-seg-sop]
## `@ohif/viewer` usage ## `@ohif/viewer` usage
We use the `sopClassHandlerModule`s in three different places: We use the `sopClassHandlerModule`s in `DisplaySetService` where we
transform instances from the raw metadata format to a OHIF displaySet format.
- `ViewerLocalFileData.js` You can read more about DisplaySetService here.
- `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`.
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html [sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html

View File

@ -1,130 +1,257 @@
# Module: Toolbar # Module: Toolbar
An extension can register a Toolbar Module by defining a `getToolbarModule` An extension can register a Toolbar Module by defining a `getToolbarModule`
method. This module is commonly used to define: method. `OHIF-v3`'s `default` extension (`"ohif.org.default"`) provides 5 main toolbar button types:
- [Toolbar buttons](#button-definitions) - [Module: Toolbar](#module-toolbar)
- [Nested toolbar menus](#nested-toolbar-menus) - [Example Toolbar Module](#example-toolbar-module)
- [Custom components](#custom-components) - [Toolbar buttons consumed in modes](#toolbar-buttons-consumed-in-modes)
- [Button Definitions](#button-definitions)
- [Nested Buttons](#nested-buttons)
- [Layout Template](#layout-template)
- [Custom Button](#custom-button)
- [Custom tool](#custom-tool)
![toolbarModule](../../assets/img/toolbarModule.png)
![Toolbar Extension](../../assets/img/extensions-toolbar.gif)
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
## Example Toolbar Module ## Example Toolbar Module
The Toolbar Module should return an array of `objects`. There are currently a few different variations of definitions,
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. each one is detailed further down.
```js ```js
export default { export default function getToolbarModule({ commandsManager, servicesManager }) {
id: 'example-toolbar-module',
/** return [
* @param {object} params {
* @param {ServicesManager} params.servicesManager name: 'ohif.divider',
* @param {CommandsManager} params.commandsManager defaultComponent: ToolbarDivider,
*/ clickHandler: () => {},
getToolbarModule({ servicesManager, commandsManager }) {
return {
definitions: [
/* Array of definitions */
],
defaultContext: ['ROUTE:VIEWER'],
};
}, },
}; {
name: 'ohif.action',
defaultComponent: ToolbarButton,
clickHandler: () => {},
},
{
name: 'ohif.radioGroup',
defaultComponent: ToolbarButton,
clickHandler: () => {},
},
{
name: 'ohif.splitButton',
defaultComponent: ToolbarSplitButton,
clickHandler: () => {},
},
{
name: 'ohif.layoutSelector',
defaultComponent: ToolbarLayoutSelector,
clickHandler: (evt, clickedBtn, btnSectionName) => {},
}
]
}
``` ```
## Toolbar buttons consumed in modes
Below we can see a simplified version of the `longitudinal` mode that shows how
a mode can add buttons to the toolbar by calling `ToolBarService.addButtons(toolbarButtons)`.
`toolbarButtons` is an array of `toolDefinitions` which we will learn next.
```js
export default function mode({ modeConfiguration }) {
return {
id: 'viewer',
displayName: 'Basic Viewer',
onModeEnter: ({ servicesManager, extensionManager }) => {
const { ToolBarService } = servicesManager.services;
ToolBarService.init(extensionManager);
ToolBarService.addButtons(toolbarButtons);
},
routes: [
{
path: 'longitudinal',
layoutTemplate: ({ location, servicesManager }) => {
return {/* */};
},
},
],
extensions: [
'org.ohif.default',
'org.ohif.cornerstone',
'org.ohif.measurement-tracking',
'org.ohif.dicom-sr',
],
};
}
```
## Button Definitions ## Button Definitions
The simplest definition has the following properties: The simplest toolbarButtons definition has the following properties:
![toolbarModule-zoom](../../assets/img/toolbarModule-zoom.png)
```js ```js
{ {
id: 'StackScroll', id: 'Zoom',
label: 'Stack Scroll', type: 'ohif.radioGroup',
icon: 'bars', props: {
type: 'setToolActive', type: 'tool',
commandName: 'setToolActive', icon: 'tool-zoom',
commandOptions: { toolName: 'StackScroll' }, label: 'Zoom',
commandOptions: { toolName: 'Zoom' },
},
}, },
``` ```
| property | description | values | | property | description | values |
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- | | ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
| `id` | Unique string identifier for the definition | \* | | `id` | Unique string identifier for the definition | \* |
| `label` | User/display friendly to show in UI | \* | | `label` | User/display friendly to show in UI | \* |
| `icon` | A string name for an icon supported by the consuming application. | \* | | `icon` | A string name for an icon supported by the consuming application. | \* |
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` | | `type` | Used to determine the button's behaviour | "tool", "toggle", "action" |
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` | | `commandName` | (optional) The command to run when the button is used. | Any command registered by a `CommandModule` |
| `commandOptions` | (optional) Options to pass the target `commandName` | \* | | `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 There are three main types of toolbar buttons:
You can indicate that buttons should be grouped and nested in a submenu by - `tool`: buttons that enable a tool by running the `setToolActive` command with the `commandOptions`
including `buttons` property in a definition: - `toggle`: buttons that acts as a toggle: e.g., linking viewports
- `action`: buttons that executes an action: e.g., capture button to save screenshot
## Nested Buttons
You can use the `ohif.splitButton` type to build a button with extra tools in the dropdown.
- First you need to give your `primary` tool definition to the split button
- the `secondary` properties can be a simple arrow down (`chevron-down` icon)
- For adding the extra tools add them to the `items` list.
You can see below how `longitudinal` mode is using the available toolbarModule to create
`MeasurementTools` nested button
![toolbarModule-nested-buttons](../../assets/img/toolbarModule-nested-buttons.png)
```js ```js
// modes/longitudinal/src/toolbarButtons.js
{ {
id: 'More', id: 'MeasurementTools',
label: 'More', type: 'ohif.splitButton',
icon: 'ellipse-circle', props: {
buttons: [ groupId: 'MeasurementTools',
isRadio: true,
primary: {
id: 'Length',
icon: 'tool-length',
label: 'Length',
type: 'tool',
commandOptions: {
toolName: 'Length',
}
},
secondary: {
icon: 'chevron-down',
label: '',
isActive: true,
tooltip: 'More Measure Tools',
},
items: [
// Length tool
{ {
id: 'cstInvert', id: 'Length',
label: 'Invert', icon: 'tool-length',
icon: 'circle', label: 'Length',
type: 'command', type: 'tool',
commandName: 'invertViewport', commandOptions: {
toolName: 'Length',
}
},
// Bidirectional tool
{
id: 'Bidirectional',
icon: 'tool-bidirectional',
label: 'Length',
type: 'tool',
commandOptions: {
toolName: 'Bidirectional',
}
},
// Ellipse tool
{
id: 'EllipticalRoi',
icon: 'tool-elipse',
label: 'Ellipse',
type: 'tool',
commandOptions: {
toolName: 'EllipticalRoi',
}
}, },
], ],
}, },
}
``` ```
![Toolbar Extension](../../assets/img/extensions-toolbar-nested.gif) <div style="padding:62.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/547957214?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Toolbar"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
<center><i>Example toolbar button demonstrating nested buttons.</i></center> ## Layout Template
Layout selector button and logic is also provided by the OHIF-v3 `default` extension.
## Custom Components To use it, you can just add the following definition to the list of `toolDefinitions`
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`.
![toolbarModule-layout](../../assets/img/toolbarModule-layout.png)
```js ```js
{ {
id: 'Custom', id: 'Layout',
label: 'Custom', type: 'ohif.layoutSelector',
icon: 'custom-icon',
CustomComponent: CustomToolbarComponent,
} }
``` ```
The `CustomComponent` components will receive the following props: <div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/545993263?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Viewer-layout"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
```html
<CustomComponent ## Custom Button
parentContext="{parentContext}" You can also create your own extension, and add your new custom tool appearance (e.g., split horizantlly instead of vertically for split tool).
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}" Simply add `getToolbarModule` to your extension, and pass your tool react component to its
button="{button}" `defaultComponent` property in the returned object.
key="{button.id}" You can use `@ohif/ui` components such as `IconButton, Icon, Tooltip, ToolbarButton` to
activeButtons="{activeButtonsIds}" build your own component.
isActive="{isActive}"
/>
```js
import myToolComponent from './myToolComponent'
export default function getToolbarModule({ commandsManager, servicesManager }) {
return [
{
name: 'new-tool-type',
defaultComponent: myToolComponent,
clickHandler: () => {},
},
]
}
``` ```
| Property | Type | Description |
| ---------------------- | -------- | ------------------------------- |
| `activeButtons` | string[] | list of active buttons | ## Custom tool
| `button` | object | its own definition object | <mark> I want to create a new tool
| `key` | string | React key prop |
| `isActive` | boolean | If current button is active |
| `parentContext` | ? | The parent component's context? |
| `toolbarClickCallback` | func | Callback method for clicks |

View File

@ -1,46 +1,95 @@
# Module: Viewport # Module: Viewport
An extension can register a Viewport Module by defining a `getViewportModule`
## Overview
Viewports consume a displaySet and display/allow the user to interact with data. An extension can register a Viewport Module by defining a `getViewportModule`
method that returns a React component. Currently, we use viewport components to method that returns a React component. Currently, we use viewport components to
add support for: add support for:
- 2D Medical Image Viewing (cornerstone ext.) - 2D Medical Image Viewing (cornerstone ext.)
- Structured Reports as HTML (dicom html ext.) - Structured Reports as SR (DICOM SR ext.)
- Encapsulated PDFs as PDFs (dicom pdf ext.) - Structured Reports as HTML (DICOM html ext.)
- Encapsulated PDFs as PDFs (DICOM pdf ext.)
- Whole Slide Microscopy Viewing (whole slide ext.) - Whole Slide Microscopy Viewing (whole slide ext.)
- etc. - 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: The general pattern is that a mode can define which `Viewport` to use for which specific
`SOPClassHandlerUID`, so if you want to fork just a single Viewport component for a specialized mode, this is possible.
```html ```JSX
<viewportComponent // displaySet, viewportIndex, dataSource
viewportData="{viewportData}" const getViewportModule = () => {
viewportIndex="{viewportIndex}" const wrappedViewport = props => {
children="{[children]}" return (
<ExampleViewport
{...props}
onEvent={data => {
commandsManager.runCommand('commandName', data);
}}
/> />
);
};
return [{ name: 'example', component: wrappedViewport }];
};
``` ```
| Property | Type | Description |
| --------------- | --------------- | --------------------------------- |
| `children` | React.element[] | |
| `viewportData` | object | `viewportSpecificData` (probably) |
| `viewportIndex` | number | |
## Example Viewport Component
A simplified version of the tracked CornerstoneViewport is shown below, which creates
a cornerstone viewport and and action bar on top of it.
```JSX
function TrackedCornerstoneViewport({
children,
dataSource,
displaySet,
viewportIndex,
servicesManager,
extensionManager,
commandsManager,
}) {
const renderViewport = () => {
const { component: Component } = extensionManager.getModuleEntry(
'org.ohif.cornerstone.viewportModule.cornerstone'
)
return (
<Component
onElementEnabled={onElementEnabled}
element={element}
{...props}
></Component>
)
}
return (
<>
<ViewportActionBar onDoubleClick onSeriesChange />
<div className="relative flex flex-row w-full h-full overflow-hidden">
{renderViewport()}
</div>
</>
)
}
```
![viewportModule](../../assets/img/viewportModule.png)
### `@ohif/viewer` ### `@ohif/viewer`
Viewport components are managed by the `ViewportGrid` Component. Which Viewport Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on: component is used depends on:
- Hanging Protocols
- The Layout Configuration - The Layout Configuration
- Registered SopClassHandlers - Registered SopClassHandlers
- The SopClassUID for visible/selected datasets
![Cornerstone Viewport](../../assets/img/extensions-viewport.png)
![viewportModule-layout](../../assets/img/viewportModule-layout.png)
<center><i>An example of three cornerstone Viewports</i></center> <center><i>An example of three cornerstone Viewports</i></center>

View File

@ -30,27 +30,4 @@ You can read more about this particular strategy in our
## Commonjs Bundle (Packaged Script) ## Commonjs Bundle (Packaged Script)
The [@ohif/viewer][viewer-npm] package is built with WebPack to provide a React We are not supporting `Commonjs` bundling inside `OHIF-v3`.
component that can be dropped into a larger application. The `OHIFViewer`
component is the entire viewer, configurable via React `props`. This is useful
for including the OHIF Viewer in a larger web application, as the entire
application can be provided via a `<script>` tag with no build process required.
The bundle is not as performant or as optimized as the PWA build. It includes
fonts, styles, and the core extensions. If you find yourself facing performance
issues, you may wish to tweak what's included in this bundle or switch to the
PWA build.
You can read more about this particular strategy in our
[Embedded Viewer Deployment Guide](./../deployment/recipes/embedded-viewer.md)
<!--
Links
-->
<!-- prettier-ignore-start -->
[pwa]: https://developers.google.com/web/progressive-web-apps/
[pwa-checklist]: https://developers.google.com/web/progressive-web-apps/checklist
[monorepo]: https://github.com/OHIF/Viewers/issues/768
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
<!-- prettier-ignore-end -->

View File

@ -0,0 +1,52 @@
<table>
<thead>
<tr>
<th>Manager</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="./extension.md">
Extension Manager
</a>
</td>
<td>
Aggregating and exposing modules and features through out the app
</td>
</tr>
<tr>
<td>
<a href="./service.md">
Services Manager
</a>
</td>
<td>
Single point of registration for all internal and external services
</td>
</tr>
<tr>
<td>
<a href="./commands.md">
Commands Manager
</a>
</td>
<td>
Register commands with specific context and run commands in the app
</td>
</tr>
<tr>
<td>
<a href="./hotkeys.md">
Hotkeys Manager
</a>
</td>
<td>
For keyboard keys assignment to commands
</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,179 @@
# Commands Manager
## Overview
The `CommandsManager` is a class defined in the `@ohif/core` project. The Commands Manager tracks named commands (or functions) that are scoped to
a context. When we attempt to run a command with a given name, we look for it
in our active contexts. If found, we run the command, passing in any application
or call specific data specified in the command's definition.
> Note: A single instance of `CommandsManager` should be defined in the consuming application, and it is used when constructing the `ExtensionManager`.
A `simplified skeleton` of the `CommandsManager` is shown below:
```js
export class CommandsManager {
constructor({ getActiveContexts } = {}) {
this.contexts = {};
this._getActiveContexts = getActiveContexts;
}
getContext(contextName) {
const context = this.contexts[contextName];
return context;
}
/**...**/
createContext(contextName) {
/** ... **/
this.contexts[contextName] = {};
}
registerCommand(contextName, commandName, definition) {
/**...**/
const context = this.getContext(contextName);
/**...**/
context[commandName] = definition;
}
runCommand(commandName, options = {}, contextName) {
const definition = this.getCommand(commandName, contextName);
/**...**/
const { commandFn } = definition;
const commandParams = Object.assign(
{},
definition.options, // "Command configuration"
options // "Time of call" info
);
/**...**/
return commandFn(commandParams);
}
/**...**/
}
```
### Instantiating
When we instantiate the `CommandsManager`, we are passing two methods:
- `getAppState` - Should return the application's state when called (Not implemented in `v3`)
- `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
// platform/viewer/src/appInit.js
const commandsManagerConfig = {
getAppState: () => {},
/** Used by commands to determine active context */
getActiveContexts: () => [
'VIEWER',
'DEFAULT',
'ACTIVE_VIEWPORT::CORNERSTONE',
],
};
const commandsManager = new CommandsManager(commandsManagerConfig);
```
## Commands/Context Registration
The `ExtensionManager` handles registering commands and creating contexts, so you don't need to register all your commands manually. Simply, create a `commandsModule` in your extension, and it will get automatically registered in the `context` provided.
A *simplified version* of this registration is shown below to give an idea about the process.
```js
export default class ExtensionManager {
constructor({ commandsManager }) {
this._commandsManager = commandsManager
}
/** ... **/
registerExtension = (extension, configuration = {}, dataSources = []) => {
let extensionId = extension.id
/** ... **/
// Register Modules provided by the extension
moduleTypeNames.forEach((moduleType) => {
const extensionModule = this._getExtensionModule(
moduleType,
extension,
extensionId,
configuration
)
if (moduleType === 'commandsModule') {
this._initCommandsModule(extensionModule)
}
/** registering other modules **/
})
}
_initCommandsModule = (extensionModule) => {
let { definitions, defaultContext } = extensionModule
defaultContext = defaultContext || 'VIEWER'
if (!this._commandsManager.getContext(defaultContext)) {
this._commandsManager.createContext(defaultContext)
}
Object.keys(definitions).forEach((commandName) => {
const commandDefinition = definitions[commandName]
const commandHasContextThatDoesNotExist =
commandDefinition.context &&
!this._commandsManager.getContext(commandDefinition.context)
if (commandHasContextThatDoesNotExist) {
this._commandsManager.createContext(commandDefinition.context)
}
this._commandsManager.registerCommand(
commandDefinition.context || defaultContext,
commandName,
commandDefinition
)
})
}
}
```
If you find yourself in a situation where you want to register a command/context manually, ask
yourself "why can't I register these commands via an extension?", but if you insist, you can use the `CommandsManager` API to do so:
```js
// Command Registration
commandsManager.registerCommand('context', 'name', commandDefinition);
// Context Creation
commandsManager.createContext('string');
```
## `CommandsManager` Public API
If you would like to run a command in the consuming app or an extension, you can
use `runCommand(commandName, options = {}, contextName)`.
```js
// Run a command, it will run all the `speak` commands in all contexts
commandsManager.runCommand('speak', { command: 'hello' });
// Run command, from Default context
commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']);
// Returns all commands for a given context
commandsManager.getContext('string');
```

Some files were not shown because too many files have changed in this diff Show More