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
2
docs/.gitignore
vendored
@ -1 +1 @@
|
||||
_book/
|
||||
./_book/*
|
||||
|
||||
@ -8,6 +8,7 @@ _Requirements:_
|
||||
|
||||
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`
|
||||
|
||||
### Editing and Previewing Changes
|
||||
|
||||
1
docs/latest/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_book/
|
||||
@ -10,11 +10,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
> ATTENTION! You are looking at the docs for the `React` version of the OHIF
|
||||
> 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
|
||||
> 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
|
||||
> of this page.
|
||||
|
||||
|
||||
# Introduction
|
||||
|
||||
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,
|
||||
annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
|
||||
|
||||

|
||||
|
||||
<center><i>The <strong>OHIF Viewer</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
|
||||
|
||||

|
||||
|
||||
<center><i>The <strong>OHIF Viewer v3</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
|
||||
|
||||
|
||||
|
||||
|
||||
## 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
|
||||
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
|
||||
our integration and extension points.
|
||||
|
||||
## Where to next?
|
||||
|
||||
Check out these helpful links:
|
||||
|
||||
@ -43,12 +77,19 @@ Check out these helpful links:
|
||||
[Learn how you can be more involved.](./development/contributing.md)
|
||||
- Feeling lost? Read our [help page](./help.md).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[ohif-org]: http://www.ohif.org
|
||||
[ohif-demo]: http://viewer.ohif.org/
|
||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
||||
[gh-issues]: https://github.com/OHIF/Viewers/issues
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@ -1,61 +1,83 @@
|
||||
# OHIF Viewers
|
||||
|
||||
- [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
|
||||
- [Getting Started](development/getting-started.md)
|
||||
- [Contributing](development/contributing.md)
|
||||
- [Continuous Integration](development/continous-integration.md)
|
||||
- [Testing](development/testing.md)
|
||||
- [Configuring](configuring/index.md)
|
||||
- [Data Source](configuring/data-source.md)
|
||||
- [Configuring Data Source](configuring/index.md)
|
||||
- [Local DICOM Server](configuring/data-source.md)
|
||||
|
||||
---
|
||||
|
||||
- [Architecture](architecture/index.md)
|
||||
- [Viewer](viewer/index.md)
|
||||
- [Configuration](viewer/configuration.md)
|
||||
- [Themeing](viewer/themeing.md)
|
||||
- [Theming](viewer/themeing.md)
|
||||
- [Internationalization](viewer/internationalization.md)
|
||||
- [Extensions](extensions/index.md)
|
||||
- [Extensions](extensions/index.md#Overview)
|
||||
- [Registering](extensions/index.md#registering-an-extension)
|
||||
- [OHIF Maintained](extensions/index.md#ohif-maintained-extensions)
|
||||
- [Lifecycle Hooks](extensions/index.md#lifecycle-hooks)
|
||||
- [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)
|
||||
- [Commands](extensions/modules/commands.md)
|
||||
- [Panel](extensions/modules/panel.md)
|
||||
- [SOP Class Handler](extensions/modules/sop-class-handler.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)
|
||||
- [Contexts](extensions/index.md#contexts)
|
||||
- [ExtensionManager](extensions/index.md#extensionmanager)
|
||||
- [OHIF Maintained](extensions/index.md#maintained-extensions)
|
||||
- [Panel](extensions/modules/panel.md)
|
||||
- [LayoutTemplate](extensions/modules/layout-template.md)
|
||||
- [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)
|
||||
- [Default](services/default/index.md)
|
||||
- [UI](services/ui/index.md)
|
||||
- [Dialog Service](services/ui/ui-dialog-service.md)
|
||||
- [Modal Service](services/ui/ui-modal-service.md)
|
||||
- [Notification Service](services/ui/ui-notification-service.md)
|
||||
|
||||
---
|
||||
- [Data Services](services/data/index.md)
|
||||
- [Dicom Metadata Store](services/data/DicomMetadataStore.md)
|
||||
- [DisplaySet Service](services/data/DisplaySetService.md)
|
||||
- [Hanging Protocol Service](services/data/HangingProtocolService.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)
|
||||
- [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)
|
||||
- [Static](deployment/recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md)
|
||||
- [User Account Control](deployment/recipes/user-account-control.md)
|
||||
- [Google Cloud Healthcare](connecting-to-image-archives/google-cloud-healthcare.md)
|
||||
- [Build for Production](deployment/recipes/build-for-production.md)
|
||||
- [Static](deployment/recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md)
|
||||
- [User Account Control](deployment/recipes/user-account-control.md)
|
||||
- [Google Cloud Healthcare](connecting-to-image-archives/google-cloud-healthcare.md)
|
||||
|
||||
---
|
||||
|
||||
- [FAQ](faq/index.md)
|
||||
- [Scope of Project](faq/scope-of-project.md)
|
||||
- [Browser Support](faq/browser-support.md)
|
||||
- [PWA vs Packaged](faq/pwa-vs-packaged.md)
|
||||
- [Help](help.md)
|
||||
- [Help](help.md) -->
|
||||
|
||||
110
docs/latest/_gap_analysis.md
Normal 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>
|
||||
|
||||
❌
|
||||
✅
|
||||
🔜
|
||||
@ -1,20 +1,26 @@
|
||||
# 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
|
||||
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
|
||||
>New `OHIF-v3` architecture has made OHIF a general purpose extensible medical imaging **platform**, as opposed to a somewhat highly configurable viewer.
|
||||
|
||||
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.
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Business Logic](#business-logic)
|
||||
- [Component Library](#react-component-library)
|
||||
- [Extensions & Configuration](#extensions--configuration)
|
||||
- [Common Questions](#common-questions)
|
||||
- [Architecture](#architecture)
|
||||
- [Overview](#overview)
|
||||
- [Extensions](#extensions)
|
||||
- [Modes](#modes)
|
||||
- [Platform](#platform)
|
||||
- [`@ohif/viewer`](#ohifviewer)
|
||||
- [`@ohif/core`](#ohifcore)
|
||||
- [`@ohif/ui`](#ohifui)
|
||||
- [Overview of the architecture](#overview-of-the-architecture)
|
||||
- [Common Questions](#common-questions)
|
||||
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
@ -25,110 +31,156 @@ you'll see the following:
|
||||
|
||||
```bash
|
||||
.
|
||||
│
|
||||
├── extensions
|
||||
│ ├── _example # Skeleton of example extension
|
||||
│ ├── cornerstone # 2D images w/ Cornerstone.js
|
||||
│ ├── dicom-html # Structured Reports as HTML in viewport
|
||||
│ ├── dicom-microscopy # Whole slide microscopy viewing
|
||||
│ ├── dicom-pdf # View DICOM wrapped PDFs in viewport
|
||||
│ └── vtk # MPR and Volume support w/ VTK.js
|
||||
│ ├── _example # Skeleton of example extension
|
||||
│ ├── default # default functionalities
|
||||
│ ├── cornerstone # 2D images w/ Cornerstone.js
|
||||
│ ├── measurement-tracking # measurement tracking
|
||||
│ ├── dicom-sr # Structured reports
|
||||
│ └── dicom-pdf # View DICOM wrapped PDFs in viewport
|
||||
│
|
||||
├── modes
|
||||
│ └── longitudinal # longitudinal measurement tracking mode
|
||||
│
|
||||
├── platform
|
||||
│ ├── core # Business Logic
|
||||
│ ├── i18n # Internationalization Support
|
||||
│ ├── ui # React component library
|
||||
│ └── viewer # Connects platform and extension projects
|
||||
│ ├── core # Business Logic
|
||||
│ ├── i18n # Internationalization Support
|
||||
│ ├── ui # React component library
|
||||
│ └── viewer # Connects platform and extension projects
|
||||
│
|
||||
├── ... # misc. shared configuration
|
||||
├── lerna.json # MonoRepo (Lerna) settings
|
||||
├── package.json # Shared devDependencies and commands
|
||||
├── ... # misc. shared configuration
|
||||
├── lerna.json # MonoRepo (Lerna) settings
|
||||
├── package.json # Shared devDependencies and commands
|
||||
└── README.md
|
||||
```
|
||||
|
||||
The `platform` directory contains the business logic library, component library,
|
||||
and the application library that combines them to create a powerful medical
|
||||
imaging viewer.
|
||||
OHIF v3 is comprised of the following components, described in detail in further sections:
|
||||
|
||||
The `extensions` directory contains many packages that can be registered with
|
||||
`@ohif/core`'s `ExtensionManager` to expand an application's supported features
|
||||
and functionality.
|
||||
- `@ohif/viewer`: The core framework that controls extension registration, mode composition and routing.
|
||||
- `@ohif/core`: A library of useful and reusable medical imaging functionality for the web.
|
||||
- `@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.
|
||||
|
||||

|
||||
|
||||
<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
|
||||
features common to Web-based medical imaging viewers. For example:
|
||||
>In `OHIF-v3`, extensions no longer automatically hook themselves to the app. Now,
|
||||
>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
|
||||
rendering logic. While we use it to power our React Viewer, it can be used with
|
||||
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,
|
||||
OHIF team maintains several high value and commonly used functionalities in its
|
||||
own extensions. For a list of extensions maintained by OHIF,
|
||||
[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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 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 OHIF’s 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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 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?
|
||||
|
||||
@ -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
|
||||
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
|
||||
-->
|
||||
|
||||
BIN
docs/latest/assets/img/OHIF-Viewer.png
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
docs/latest/assets/img/OHIF-e2e-test-studies.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 37 KiB |
BIN
docs/latest/assets/img/custom-logo.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 118 KiB |
BIN
docs/latest/assets/img/docker-pacs.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/latest/assets/img/e2e-cypress-final.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
docs/latest/assets/img/e2e-cypress.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 440 KiB |
|
Before Width: | Height: | Size: 20 MiB |
|
Before Width: | Height: | Size: 230 KiB |
BIN
docs/latest/assets/img/mode-archs.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 994 KiB |
|
Before Width: | Height: | Size: 99 KiB |
BIN
docs/latest/assets/img/panel-module-left-right.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/latest/assets/img/panel-module-v3.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/latest/assets/img/panelmodule-icon.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/latest/assets/img/services-data.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/latest/assets/img/services-measurements.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/latest/assets/img/services-ui.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/latest/assets/img/toolbarModule-layout.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
docs/latest/assets/img/toolbarModule-nested-buttons.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/latest/assets/img/toolbarModule-zoom.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
docs/latest/assets/img/toolbarModule.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/latest/assets/img/ui-modal.gif
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
docs/latest/assets/img/user-hotkeys-default.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/latest/assets/img/user-hotkeys.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/latest/assets/img/user-measurement-edit-name.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/latest/assets/img/user-measurement-edit.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/latest/assets/img/user-measurement-export.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/latest/assets/img/user-measurement-name.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/latest/assets/img/user-measurement-panel-modal.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
docs/latest/assets/img/user-measurement-panel-tracked.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
docs/latest/assets/img/user-measurement-prevnext.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
docs/latest/assets/img/user-open-viewer.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/latest/assets/img/user-study-filter.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/latest/assets/img/user-study-list.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
docs/latest/assets/img/user-study-next.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/latest/assets/img/user-study-panel.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
docs/latest/assets/img/user-study-summary.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/latest/assets/img/user-studyist-modespecific.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/latest/assets/img/user-studylist-all.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/latest/assets/img/user-toolbar-download-icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/latest/assets/img/user-toolbar-download.png
Normal file
|
After Width: | Height: | Size: 153 KiB |
BIN
docs/latest/assets/img/user-toolbar-extra.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/latest/assets/img/user-toolbar-preset.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/latest/assets/img/user-viewer-components.png
Normal file
|
After Width: | Height: | Size: 506 KiB |
BIN
docs/latest/assets/img/user-viewer-layout.png
Normal file
|
After Width: | Height: | Size: 370 KiB |
BIN
docs/latest/assets/img/user-viewer-main.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
docs/latest/assets/img/user-viewer-toolbar-measurements.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
docs/latest/assets/img/user-viewer-toolbar.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
docs/latest/assets/img/user-viewer.png
Normal file
|
After Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 662 KiB |
BIN
docs/latest/assets/img/viewportModule-layout.png
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
docs/latest/assets/img/viewportModule.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 104 KiB |
@ -35,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
"value": "https://docs.ohif.org/history/v2/",
|
||||
"text": "Version 2.0.0",
|
||||
"text": "Version 2.0.0"
|
||||
},
|
||||
{
|
||||
"value": "https://docs.ohif.org/",
|
||||
|
||||
@ -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
|
||||
|
||||
> ATTENTION! Already have a remote or local server? Skip to the
|
||||
> [configuration section](#configuration-learn-more) below.
|
||||
- [Set up a local DICOM server](#set-up-a-local-dicom-server)
|
||||
- [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
|
||||
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]
|
||||
> 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
|
||||
|
||||
_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
|
||||
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
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```js
|
||||
@ -114,51 +133,54 @@ configuration looks like:
|
||||
```js
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'Orthanc',
|
||||
wadoUriRoot: 'http://localhost:8899/wado',
|
||||
qidoRoot: 'http://localhost:8899/dicom-web',
|
||||
wadoRoot: 'http://localhost:8899/dicom-web',
|
||||
qidoSupportsIncludeField: false,
|
||||
extensions: [],
|
||||
modes: [],
|
||||
showStudyList: true,
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'dcmjs DICOMWeb Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
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',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: true,
|
||||
supportsWildcard: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
};
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[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 |
|
||||
| --------------------------------------------- | ---------------------------------- |
|
||||
| [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 |
|
||||
### Running DCM4CHEE
|
||||
|
||||
dcm4che is a collection of open source applications for healthcare enterprise written in Java programming language
|
||||
which implements DICOM standard. dcm4chee (extra 'e' at the end) is dcm4che project
|
||||
for an Image Manager/Image Archive which provides storage, retrieval and other functionalities.
|
||||
You can read more about dcm4chee in their website [here](https://www.dcm4che.org/)
|
||||
|
||||
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&autopause=0&player_id=0&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-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
|
||||
[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
|
||||
[osirix]: http://www.osirix-viewer.com/
|
||||
[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
|
||||
|
||||
@ -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
|
||||
|
||||
- [Overview](#overview)
|
||||
|
||||
- [Configuring Data Source](#configuring-data-source)
|
||||
- [Overview](#overview)
|
||||
- [Configuration Files](#configuration-files)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [How do I configure my project?](#how-do-i-configure-my-project)
|
||||
|
||||
|
||||
## 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`
|
||||
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,
|
||||
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:
|
||||
|
||||
_/platform/viewer/public/config/default.js_
|
||||
> _/platform/viewer/public/config/default.js_
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
extensions: [],
|
||||
modes: [],
|
||||
showStudyList: true,
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'dcmjs DICOMWeb Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
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',
|
||||
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 OHIF’s > 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:
|
||||
|
||||
|
||||
```js
|
||||
window.config = ({ servicesManager } = {}) => {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
@ -82,23 +94,59 @@ window.config = ({ servicesManager } = {}) => {
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
],
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'dcmjs DICOMWeb Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
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',
|
||||
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
|
||||
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
||||
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
|
||||
output.
|
||||
|
||||
|
||||
|
||||
<!--
|
||||
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-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
|
||||
[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
|
||||
|
||||
@ -1,71 +1,3 @@
|
||||
# Google Cloud Healthcare
|
||||
|
||||
> The [Google Cloud Healthcare API](https://cloud.google.com/healthcare/) is a
|
||||
> 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
|
||||
```
|
||||
> Coming soon - We are working on bringing Google Cloud Healthcare to OHIF-v3
|
||||
|
||||
@ -16,17 +16,10 @@ applications. If you find yourself a little lost, please don't hesitate to
|
||||
|
||||
## Deployment Scenarios
|
||||
|
||||
### 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.
|
||||
|
||||
- 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" %}
|
||||
### Embedded Viewer (deprecated)
|
||||
`OHIF-v3` has deprecated deploying the viewer as an embedded viewer the number of underlying
|
||||
libraries that run web workers are increasing for OHIF. An example of these libraries is
|
||||
OHIF's 3D rendering functionality that is provided by `vtk-js`.
|
||||
|
||||
### Stand-alone Viewer
|
||||
|
||||
@ -35,7 +28,7 @@ benefits, but comes at the cost of time and complexity. Some benefits include:
|
||||
|
||||
_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
|
||||
- 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?
|
||||
|
||||
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
|
||||
[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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
You can read more about using different data sources for mode's routes [here](../modes/routes.md#route-path)
|
||||
|
||||
*Sample JSON format:*
|
||||
```JSON
|
||||
@ -199,11 +194,11 @@ More info on this JSON format can be found here [Issue #1500](https://github.com
|
||||
|
||||
**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.
|
||||
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.
|
||||
eg. `https://image-server/my-image.dcm` ---> `dicomweb: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.
|
||||
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).
|
||||
<!-- 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. 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` ---> `dicomjson:https://image-server/my-image.dcm`
|
||||
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.
|
||||
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)*
|
||||
|
||||
@ -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`
|
||||
|
||||
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.
|
||||
|
||||
@ -230,7 +225,9 @@ Essentially, whenever the application connects to a remote resource, you will ne
|
||||
|
||||
|
||||
### Securing Your Data
|
||||
Coming soon
|
||||
|
||||
<!--
|
||||
> 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
|
||||
> [enlist help from experts](./../help.md).
|
||||
@ -273,19 +270,19 @@ 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
|
||||
to secure access to your Image Archive as well.
|
||||
|
||||
-->
|
||||
|
||||
### Recipes
|
||||
|
||||
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.
|
||||
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)
|
||||
- [Static](./recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
|
||||
- [User Account Control](./recipes/user-account-control.md)
|
||||
|
||||
- [Build for Production](./recipes/build-for-production.md)
|
||||
- [Static](./recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
|
||||
- [User Account Control](./recipes/user-account-control.md)
|
||||
|
||||
<!--
|
||||
Links
|
||||
|
||||
@ -104,14 +104,18 @@ directory:
|
||||
// Install http-server as a globally available package
|
||||
yarn global add http-server
|
||||
|
||||
// Change the directory to the platform/viewer
|
||||
|
||||
// Serve the files in our current directory
|
||||
// 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&autopause=0&player_id=0&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
|
||||
|
||||
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
|
||||
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:
|
||||
@ -120,11 +124,7 @@ web application. For a starting point, check out this repository's own use of:
|
||||
- [Netlify][netlify]: [netlify.toml][netlify.toml] |
|
||||
[build-deploy-preview.sh][build-deploy-preview.sh]
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
> Issues and resolutions for common GitHub issues will be summarized here
|
||||
|
||||
...
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[circleci]: https://circleci.com/gh/OHIF/Viewers
|
||||
|
||||
@ -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 -->
|
||||
@ -79,6 +79,20 @@ in command prompt or terminal_
|
||||
|
||||
### 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&autopause=0&player_id=0&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_
|
||||
|
||||
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
|
||||
@ -90,7 +104,7 @@ _Upload Your First Study_
|
||||
- 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
|
||||
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List -->
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@ -110,6 +124,14 @@ Stop running all containers:
|
||||
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
|
||||
- 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
|
||||
|
||||
After verifying that everything runs with default configuration values, you will
|
||||
|
||||
@ -25,19 +25,23 @@ _Easy_
|
||||
|
||||
_Advanced_
|
||||
|
||||
- [AWS S3 + Cloudfront](#aws-s3--cloudfront)
|
||||
- [GCP + Cloudflare](#gcp--cloudflare)
|
||||
- [Azure](#azure)
|
||||
- [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)
|
||||
- [GCP + Cloudflare](#gcp--cloudflare)
|
||||
- [Azure](#azure)
|
||||
|
||||
## Drag-n-drop
|
||||
|
||||
### Netlify: Drop
|
||||
|
||||
> Build, deploy, and manage modern web projects. An all-in-one workflow that
|
||||
> combines global deployment, continuous integration, and automatic HTTPS. And
|
||||
> that’s just the beginning.
|
||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/551957463?badge=0&autopause=0&player_id=0&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>
|
||||
|
||||

|
||||
|
||||
_GIF demonstrating deployment with Netlify Drop_
|
||||
|
||||
|
||||
@ -1,288 +1,3 @@
|
||||
# User Account Control
|
||||
|
||||
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in
|
||||
> 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 |
|
||||
| ---------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
|  |  |
|
||||
|
||||
## 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 -->
|
||||
> Coming soon - We are working on bringing the User Account Control to OHIF-v3
|
||||
|
||||
@ -43,11 +43,13 @@ On your local file system:
|
||||
|
||||
- Open a terminal/shell
|
||||
- Navigate to `cornerstonejs/cornerstone-tools`
|
||||
- `npm install`
|
||||
- `yarn install`
|
||||
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `npm run dev`
|
||||
- `yarn run dev`
|
||||
|
||||
|
||||
- Open a new terminal/shell
|
||||
- Navigate to `ohif/viewers`.
|
||||
- Navigate to `ohif/viewers` (the root of ohif project)
|
||||
- `yarn install`
|
||||
- [`yarn link cornerstone-tools`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `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 install --force`
|
||||
|
||||
<!--
|
||||
### `react-vtkjs-viewport`
|
||||
|
||||
On your local file system:
|
||||
@ -88,7 +91,7 @@ On your local file system:
|
||||
- Navigate to `ohif/viewers`.
|
||||
- `yarn install`
|
||||
- [`yarn link react-vtkjs-viewport`](https://yarnpkg.com/en/docs/cli/link)
|
||||
- `yarn run dev`
|
||||
- `yarn run dev` -->
|
||||
|
||||
#### 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
|
||||
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?
|
||||
|
||||
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
|
||||
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
|
||||
autogenerated and available for testing.
|
||||
@ -133,7 +140,7 @@ the gitbook generated by the pull request:
|
||||
|
||||
```bash
|
||||
https://deploy-preview-237--ohif.netlify.com/contributing.html
|
||||
```
|
||||
``` -->
|
||||
|
||||
<!--
|
||||
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
|
||||
[pr-237]: https://github.com/OHIF/Viewers/pull/237
|
||||
[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 -->
|
||||
|
||||
@ -47,6 +47,9 @@ Navigate to the root of the project's directory in your terminal and run the
|
||||
following commands:
|
||||
|
||||
```bash
|
||||
# Switch to the v3 branch
|
||||
git switch feat/v2-main
|
||||
|
||||
# Restore dependencies
|
||||
yarn install
|
||||
|
||||
@ -67,10 +70,7 @@ You should see the following output:
|
||||
|
||||
### 🎉 Celebrate 🎉
|
||||
|
||||
<center>
|
||||
<img alt="development server hosted app" src="/assets/img/loading-study.gif" />
|
||||
<i>Our app, hosted by the development server</i>
|
||||
</center>
|
||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/545988245?badge=0&autopause=0&player_id=0&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>
|
||||
|
||||
### Building for Production
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Testing Philosiphy
|
||||
|
||||
> 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.
|
||||
|
||||
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 -->
|
||||
[eslint-rules]: https://eslint.org/docs/rules/
|
||||
[mini-pacs]: https://github.com/OHIF/viewer-testdata
|
||||
[typescript-docs]: https://www.typescriptlang.org/docs/home.html
|
||||
[flow-org]: https://flow.org/
|
||||
<!-- Talks -->
|
||||
|
||||
@ -7,6 +7,19 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<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 -->
|
||||
<tr>
|
||||
<td>
|
||||
@ -15,34 +28,12 @@
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>cornerstone.js</code>. Adds support for 2D DICOM rendering and manipulation, as well as support for the tools features in <a href="https://tools.cornerstonejs.org/examples/"><code>cornerstone-tools</code></a>. Also adds "CINE Dialog" to the Toolbar.
|
||||
Provides rendering functionalities for 2D images.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<!-- VTK.js -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-vtk">
|
||||
VTK.js
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>vtk.js</code>. Adds support for volume renderings and advanced features like MPR. Also adds "3D Rotate" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<!-- 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>
|
||||
<td>ViewportModule, CommandsModule</td>
|
||||
</tr>
|
||||
<!-- dicom-pdf -->
|
||||
<tr>
|
||||
<!-- <tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
|
||||
</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>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<!-- dicom-microscopy -->
|
||||
</tr> -->
|
||||
<!-- dicom-sr -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
|
||||
<a href="">DICOM SR</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders Microscopy images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7">specific SopClassUID</a>.
|
||||
Maintained extensions for cornerstone and visualization of DICOM Structured Reports
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
<td>ViewportModule, CommandsModule, SOPClassHandlerModule</td>
|
||||
</tr>
|
||||
<!-- dicom-segmentation -->
|
||||
<!-- Measurement-tracking -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-segmentation">DICOM Segmentation</a>
|
||||
<a href="">Measurement tracking</a>
|
||||
</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>Panel, Toolbar</td>
|
||||
<td> ContextModule,PanelModule,ViewportModule,CommandsModule</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
94
docs/latest/extensions/_modules.md
Normal 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>
|
||||
@ -1,53 +1,50 @@
|
||||
# Extensions
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Concepts](#concepts)
|
||||
- [Extensions](#extensions)
|
||||
- [Overview](#overview)
|
||||
- [Extension Skeleton](#extension-skeleton)
|
||||
- [OHIF-Maintained Extensions](#ohif-maintained-extensions)
|
||||
- [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)
|
||||
- [Contexts](#contexts)
|
||||
- [Consuming Extensions](#consuming-extensions)
|
||||
- [Extension Manager](#extensionmanager)
|
||||
- [Maintained Extensions](#maintained-extensions)
|
||||
- [Contexts](#contexts)
|
||||
|
||||
## 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.
|
||||
Extensions provide functionality, ui components, and new behaviors. Ideally,
|
||||
they're built in a way that allows them to extend entirely different
|
||||
implementations of the `@ohif/viewer` project.
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
> - 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;">
|
||||
<a href="/assets/img/extensions-diagram.png">
|
||||
<img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Diagram showing how extensions are configured and accessed.</i></div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
The `@ohif/viewer`'s application level configuration gives us the ability to add
|
||||
and configure extensions. When the application starts, extensions are registered
|
||||
with the `ExtensionManager`. Different portions of the `@ohif/viewer` project
|
||||
will use registered extensions to influence application behavior.
|
||||
|
||||
Extensions allow us to:
|
||||
|
||||
- Wrap and integrate functionality of 3rd party dependencies in a reusable way
|
||||
- Change how application data is mapped and transformed
|
||||
- Display a consistent/cohesive UI
|
||||
- Inject custom components to override built-in components
|
||||
|
||||
Practical examples of extensions include:
|
||||
|
||||
- A set of segmentation tools that build on top of the `cornerstone` viewport
|
||||
- Showing ML/AI report summaries for the selected study/series/image
|
||||
- Support for parsing DICOM structured reports and displaying them in a user
|
||||
friendly way
|
||||
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
|
||||
|
||||
## Concepts
|
||||
|
||||
### Extension Skeleton
|
||||
## Extension Skeleton
|
||||
|
||||
An extension is a plain JavaScript object that has an `id` property, and one or
|
||||
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
|
||||
@ -62,45 +59,77 @@ export default {
|
||||
|
||||
// Lifecyle
|
||||
preRegistration() { /* */ },
|
||||
onModeEnter() { /* */ },
|
||||
onModeExit() { /* */ },
|
||||
// Modules
|
||||
getCommandsModule() { /* */ },
|
||||
getToolbarModule() { /* */ },
|
||||
getLayoutTemplateModule() { /* */ },
|
||||
getDataSourcesModule() { /* */ },
|
||||
getSopClassHandlerModule() { /* */ },
|
||||
getPanelModule() { /* */ },
|
||||
getSopClassHandler() { /* */ },
|
||||
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
|
||||
[build time](#registering-at-build-time).
|
||||
|
||||
You can leverage one or both strategies. Which one(s) you choose depend on your
|
||||
application's requirements. Each [module](#modules) defined by the extension
|
||||
becomes available to the core application via the `ExtensionManager`.
|
||||
application's requirements.
|
||||
|
||||
#### 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
|
||||
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.
|
||||
|
||||
```js
|
||||
import MyFirstExtension from '@ohif/extension-first'
|
||||
import MySecondExtension from '@ohif/extension-second'
|
||||
|
||||
const extensionConfig = {/* extension configuration */}
|
||||
|
||||
// prettier-ignore
|
||||
const config = {
|
||||
routerBasename: '/',
|
||||
extensions: [
|
||||
MyFirstExtension,
|
||||
[
|
||||
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"
|
||||
(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
|
||||
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)
|
||||
|
||||
If an extension defines the [`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)
|
||||
lifecycle hook, it is called before any modules are registered in the
|
||||
`ExtensionManager`. It's most commonly used to wire up extensions to
|
||||
[services](./../services/index.md) and [commands](./modules/commands.md), and to
|
||||
bootstrap 3rd party libraries.
|
||||
|
||||
### Modules
|
||||
|
||||
Modules are the meat of extensions. They provide "definitions", components, and
|
||||
filtering/mapping logic that are then made available by various managers and
|
||||
services.
|
||||
[`onModeEnter`](./lifecycle/on-mode-enter.md): This hook is called whenever a new mode is entered, or a mode’s data or datasource is switched. This hook can be used to initialize data.
|
||||
|
||||
[`onModeExit`](./lifecycle/on-mode-exit.md): Similarly to onModeEnter, this hook is called when navigating away from a mode, or before a mode’s 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
|
||||
differently.
|
||||
|
||||
| Type | Description | Examples |
|
||||
| ------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| [Commands](./modules/commands.md) | Adds named commands, scoped to a context, to the CommandsManager | `setToolActive()`, `nextSeries()` |
|
||||
| [Panel](./modules/panel.md) | Adds left or right hand side panels | `<ThumbnailList />`, `<MeasurementsTable />` |
|
||||
| [SOPClassHandler](./modules/sop-class-handler.md) | Determines how retrieved study data is split into "DisplaySets" | `getDisplaySetFromSeries()` |
|
||||
| [Toolbar](./modules/toolbar.md) | Adds buttons or custom components to the toolbar | Toolbar button, nested buttons, custom |
|
||||
| [Viewport](./modules/viewport.md) | Adds a component responsible for rendering a "DisplaySet" | `<CornerstoneViewport />`, `<DicomPdfViewport />` |
|
||||
|
||||
{% include "./_modules.md" %}
|
||||
|
||||
|
||||
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
|
||||
|
||||
|
||||
|
||||
|
||||
### Contexts
|
||||
|
||||
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
|
||||
@ -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"
|
||||
are used to determine the appropriate implementation of the rotate behavior.
|
||||
|
||||
|
||||
<!-- <mark>do we want the followings?
|
||||
## Consuming Extensions
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
29
docs/latest/extensions/lifecycle/on-mode-enter.md
Normal 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;
|
||||
});
|
||||
},
|
||||
};
|
||||
```
|
||||
17
docs/latest/extensions/lifecycle/on-mode-exit.md
Normal 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()
|
||||
},
|
||||
};
|
||||
```
|
||||
@ -13,9 +13,30 @@ The `preRegistration` hook receives an object containing the
|
||||
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
|
||||
`configuration` that was provided with the extension at time of registration.
|
||||
|
||||
_Example `preRegistration` hook implementation_
|
||||
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
|
||||
|
||||
// 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 {
|
||||
id: 'MyExampleExtension',
|
||||
|
||||
@ -35,6 +56,10 @@ export default {
|
||||
|
||||
console.log('Important stuff has been wired.');
|
||||
window.importantStuff();
|
||||
|
||||
// Registering new services
|
||||
servicesManager.registerService(MyNewService(servicesManager));
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
# Module: Commands
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Command Definitions](#command-definitions)
|
||||
- [Commands Manager](#commands-manager)
|
||||
- [Instantiating](#instatiating)
|
||||
- [Public API](#public-api)
|
||||
- [Contexts](#contexts)
|
||||
|
||||
|
||||
- [Module: Commands](#module-commands)
|
||||
- [Overview](#overview)
|
||||
- [Command Definitions](#command-definitions)
|
||||
- [Command Behavior](#command-behavior)
|
||||
- [`CommandsManager` Public API](#commandsmanager-public-api)
|
||||
- [Contexts](#contexts)
|
||||
|
||||
## 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`
|
||||
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
|
||||
- Only the correct command's implementation will be run, dependent on the
|
||||
application's "context"
|
||||
- Commands can be called from extensions, modules, and the consuming application
|
||||
- Commands are used by hotkeys, toolbar buttons and render settings
|
||||
|
||||
Here is a simple example commands module:
|
||||
|
||||
```js
|
||||
export default {
|
||||
id: 'example-commands-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
definitions: {
|
||||
sayHello: {
|
||||
commandFn: ({ words }) => {
|
||||
console.log(words);
|
||||
},
|
||||
options: { words: 'Hello!' },
|
||||
},
|
||||
const getCommandsModule = () => ({
|
||||
definitions: {
|
||||
exampleActionDef: {
|
||||
commandFn: ({ param1 }) => {
|
||||
console.log(`param1's value is: ${param1}`);
|
||||
},
|
||||
defaultContext: 'VIEWER',
|
||||
};
|
||||
// storeContexts: ['viewports'],
|
||||
options: { param1: 'param1' },
|
||||
context: 'VIEWER', // optional
|
||||
},
|
||||
},
|
||||
};
|
||||
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
Each definition returned by the Commands Module is registered to the
|
||||
`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
|
||||
|
||||
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`
|
||||
is the "command" that is actioned.
|
||||
is the "command" that is actioned. T
|
||||
|
||||
```js
|
||||
myCommandName: {
|
||||
commandFn: ({ viewports, other, options }) => { },
|
||||
storeContexts: ['viewports'],
|
||||
options: { words: 'Just kidding! Goodbye!' },
|
||||
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
exampleActionDef: {
|
||||
commandFn: ({ param1, options }) => { },
|
||||
options: { param1: 'measurement' },
|
||||
context: 'DEFAULT',
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
|
||||
| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instatiation. |
|
||||
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
|
||||
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
|
||||
|
||||
## Command Behavior
|
||||
|
||||
**I have many similar commands. How can I share their `commandFn` and make it
|
||||
reusable?**
|
||||
|
||||
This is where `storeContexts` and `options` come in. We use these in our
|
||||
`setToolActive` command. `storeContexts` helps us identify our `activeViewport`,
|
||||
and `options` allow us to pass in the name of a tool we would like to set as
|
||||
active.
|
||||
|
||||
**If there are multiple valid commands for the application's active contexts**
|
||||
|
||||
@ -93,45 +84,21 @@ active.
|
||||
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
|
||||
current viewport (PDF or HTML)
|
||||
|
||||
## `CommandsManager`
|
||||
|
||||
The `CommandsManager` is a class defined in the `@ohif/core` project. A single
|
||||
instance of it should be defined in the consuming application, and it should be
|
||||
used when constructing the `ExtensionManager`.
|
||||
|
||||
### Instantiating
|
||||
|
||||
When we instantiate the `CommandsManager`, we need to pass it two methods:
|
||||
|
||||
- `getAppState` - Should return the application's state when called
|
||||
- `getActiveContexts` - Should return the application's active contexts when
|
||||
called
|
||||
|
||||
These methods are used internally to help determine which commands are currently
|
||||
valid, and how to provide them with any state they may need at the time they are
|
||||
called.
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager({
|
||||
getAppState,
|
||||
getActiveContexts,
|
||||
});
|
||||
```
|
||||
|
||||
### Public API
|
||||
## `CommandsManager` Public API
|
||||
|
||||
If you would like to run a command in the consuming app or an extension, you can
|
||||
use one of the following methods:
|
||||
use `CommandsManager.runCommand(commandName, options = {}, contextName)`
|
||||
|
||||
|
||||
```js
|
||||
// Returns all commands for a given context
|
||||
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' });
|
||||
|
||||
// Run command, but override the active contexts
|
||||
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']);
|
||||
// Run command, from Default context
|
||||
commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']);
|
||||
```
|
||||
|
||||
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
|
||||
likely publish guidance around creating contexts, and ways to override extension
|
||||
defined contexts in the near future. If you would like to discuss potential
|
||||
changes to how contexts work, please don't hesistate to createa new GitHub
|
||||
changes to how contexts work, please don't hesitate to create a new GitHub
|
||||
issue.
|
||||
|
||||
[Some additional information on Contexts can be found here.](./../index.md#contexts)
|
||||
|
||||
30
docs/latest/extensions/modules/contextModule.md
Normal 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 mode’s 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,
|
||||
},
|
||||
];
|
||||
```
|
||||
85
docs/latest/extensions/modules/data-source.md
Normal file
@ -0,0 +1,85 @@
|
||||
# Module: Data Source
|
||||
|
||||
|
||||
|
||||
## Overview
|
||||
The internal data structure of OHIF’s 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 OHIF’s 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 OHIF’s native format, then use any existing mode on your platform. Your data doesn’t 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.
|
||||
52
docs/latest/extensions/modules/hpModule.md
Normal 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],
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
136
docs/latest/extensions/modules/layout-template.md
Normal 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&autopause=0&player_id=0&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>
|
||||
@ -1,13 +1,16 @@
|
||||
# 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.
|
||||
The panel module provides the ability to define `menuOptions` and `components`
|
||||
that can be used by the consuming application. `components` are React Components
|
||||
that can be displayed in the consuming application's "Panel" Component.
|
||||
|
||||

|
||||
|
||||
<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
|
||||
`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
|
||||
associated `ServicesManager` and `CommandsManager`.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import MyComponent from './MyComponent.js';
|
||||
import PanelMeasurementTable from './PanelMeasurementTable.js';
|
||||
|
||||
|
||||
function getPanelModule({
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
}) {
|
||||
const wrappedMeasurementPanel = () => {
|
||||
return (
|
||||
<PanelMeasurementTable
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
id: 'example-panel-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
// A suggested icon
|
||||
// Available icons determined by consuming app
|
||||
icon: 'list',
|
||||
// A suggested label
|
||||
label: 'Magic',
|
||||
// 'right' or 'left'
|
||||
from: 'right',
|
||||
// The target component to toggle open/close
|
||||
target: 'target-component-id',
|
||||
// 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'],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
# 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]
|
||||
Handler is a bit different from the other modules, as it doesn't provide a `1:1`
|
||||
|
||||
## Overview
|
||||
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:
|
||||
|
||||
- `sopClassUIDs`: an array of string SOP Class UIDs that the
|
||||
@ -14,87 +19,82 @@ A `displaySet` has the following shape:
|
||||
|
||||
```js
|
||||
return {
|
||||
plugin: 'html',
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
Modality: 'MR',
|
||||
displaySetInstanceUIDD
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
SeriesNumber,
|
||||
FrameRate,
|
||||
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
|
||||
|
||||
```js
|
||||
const SOP_CLASS_UIDS = {
|
||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||
|
||||
|
||||
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',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUIDs: Object.values(SOP_CLASS_UIDS),
|
||||
// It is important to note that the used SOPClassUIDs in the modes are in the order that is specified in the array.
|
||||
const sopClassUids = [
|
||||
sopClassDictionary.CTImageStorage,
|
||||
sopClassDictionary.MRImageStorage,
|
||||
;
|
||||
|
||||
/**
|
||||
* @param {object} series -
|
||||
* @param {object} study -
|
||||
* @param {object} dicomWebClient -
|
||||
* @param {object} authorizationHeaders -
|
||||
*/
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
const makeDisplaySet = (instances) => {
|
||||
const instance = instances[0];
|
||||
const imageSet = new ImageSet(instances);
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
imageSet.setAttributes({
|
||||
displaySetInstanceUID: imageSet.uid,
|
||||
SeriesDate: instance.SeriesDate,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
FrameRate: instance.FrameTime,
|
||||
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 :
|
||||
|
||||
- [Dicom-HTML SOP][dicom-html-sop]
|
||||
- [Dicom-PDF SOP][dicom-pdf-sop]
|
||||
- [Dicom-Microscopy SOP][dicom-micro-sop]
|
||||
- [Dicom-Segmentation SOP][dicom-seg-sop]
|
||||
You can find another example for this mapping between raw metadata and displaySet for
|
||||
`DICOM-SR` extension.
|
||||
|
||||
## `@ohif/viewer` usage
|
||||
|
||||
We use the `sopClassHandlerModule`s in three different places:
|
||||
|
||||
- `ViewerLocalFileData.js`
|
||||
- `ViewerRetrieveStudyData.js`
|
||||
- `StandaloneRouting.js`
|
||||
|
||||
Each time, it is used to map study and series data to `displaySets`. It does
|
||||
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
|
||||
manager has the method `createDisplaySets` that takes an array of
|
||||
`sopClassHandlerModules`.
|
||||
We use the `sopClassHandlerModule`s in `DisplaySetService` where we
|
||||
transform instances from the raw metadata format to a OHIF displaySet format.
|
||||
You can read more about DisplaySetService here.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html
|
||||
|
||||
@ -1,130 +1,257 @@
|
||||
# Module: Toolbar
|
||||
|
||||
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)
|
||||
- [Nested toolbar menus](#nested-toolbar-menus)
|
||||
- [Custom components](#custom-components)
|
||||
- [Module: Toolbar](#module-toolbar)
|
||||
- [Example Toolbar Module](#example-toolbar-module)
|
||||
- [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)
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
|
||||
|
||||
## Example Toolbar Module
|
||||
|
||||
The Toolbar Module should return an array of `definitions` and a
|
||||
`defaultContext`. There are currently a few different variations of definitions,
|
||||
The Toolbar Module should return an array of `objects`. There are currently a few different variations of definitions,
|
||||
each one is detailed further down.
|
||||
|
||||
```js
|
||||
export default {
|
||||
id: 'example-toolbar-module',
|
||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getToolbarModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
definitions: [
|
||||
/* Array of definitions */
|
||||
],
|
||||
defaultContext: ['ROUTE:VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
return [
|
||||
{
|
||||
name: 'ohif.divider',
|
||||
defaultComponent: ToolbarDivider,
|
||||
clickHandler: () => {},
|
||||
},
|
||||
{
|
||||
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
|
||||
|
||||
The simplest definition has the following properties:
|
||||
The simplest toolbarButtons definition has the following properties:
|
||||
|
||||

|
||||
|
||||
```js
|
||||
{
|
||||
id: 'StackScroll',
|
||||
label: 'Stack Scroll',
|
||||
icon: 'bars',
|
||||
type: 'setToolActive',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'StackScroll' },
|
||||
id: 'Zoom',
|
||||
type: 'ohif.radioGroup',
|
||||
props: {
|
||||
type: 'tool',
|
||||
icon: 'tool-zoom',
|
||||
label: 'Zoom',
|
||||
commandOptions: { toolName: 'Zoom' },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
|
||||
| property | description | values |
|
||||
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `id` | Unique string identifier for the definition | \* |
|
||||
| `label` | User/display friendly to show in UI | \* |
|
||||
| `icon` | A string name for an icon supported by the consuming application. | \* |
|
||||
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` |
|
||||
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` |
|
||||
| `type` | Used to determine the button's behaviour | "tool", "toggle", "action" |
|
||||
| `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` | \* |
|
||||
| `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
|
||||
including `buttons` property in a definition:
|
||||
- `tool`: buttons that enable a tool by running the `setToolActive` command with the `commandOptions`
|
||||
- `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
|
||||
|
||||

|
||||
|
||||
```js
|
||||
// modes/longitudinal/src/toolbarButtons.js
|
||||
|
||||
{
|
||||
id: 'More',
|
||||
label: 'More',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
id: 'cstInvert',
|
||||
label: 'Invert',
|
||||
icon: 'circle',
|
||||
type: 'command',
|
||||
commandName: 'invertViewport',
|
||||
id: 'MeasurementTools',
|
||||
type: 'ohif.splitButton',
|
||||
props: {
|
||||
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: 'Length',
|
||||
icon: 'tool-length',
|
||||
label: 'Length',
|
||||
type: 'tool',
|
||||
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',
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
<div style="padding:62.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/547957214?badge=0&autopause=0&player_id=0&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>
|
||||
|
||||
## Custom Components
|
||||
|
||||
The Toolbar Modules supports rendering custom components in place of the
|
||||
application's default. In place of the `type`, `commandName`, and
|
||||
`commandOptions` properties, we instead specify a `CustomComponent`.
|
||||
## Layout Template
|
||||
Layout selector button and logic is also provided by the OHIF-v3 `default` extension.
|
||||
To use it, you can just add the following definition to the list of `toolDefinitions`
|
||||
|
||||

|
||||
```js
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
id: 'Layout',
|
||||
type: 'ohif.layoutSelector',
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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&autopause=0&player_id=0&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
|
||||
parentContext="{parentContext}"
|
||||
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}"
|
||||
button="{button}"
|
||||
key="{button.id}"
|
||||
activeButtons="{activeButtonsIds}"
|
||||
isActive="{isActive}"
|
||||
/>
|
||||
|
||||
## Custom Button
|
||||
You can also create your own extension, and add your new custom tool appearance (e.g., split horizantlly instead of vertically for split tool).
|
||||
Simply add `getToolbarModule` to your extension, and pass your tool react component to its
|
||||
`defaultComponent` property in the returned object.
|
||||
You can use `@ohif/ui` components such as `IconButton, Icon, Tooltip, ToolbarButton` to
|
||||
build your own component.
|
||||
|
||||
|
||||
```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 |
|
||||
| `button` | object | its own definition object |
|
||||
| `key` | string | React key prop |
|
||||
| `isActive` | boolean | If current button is active |
|
||||
| `parentContext` | ? | The parent component's context? |
|
||||
| `toolbarClickCallback` | func | Callback method for clicks |
|
||||
|
||||
|
||||
## Custom tool
|
||||
<mark> I want to create a new tool
|
||||
|
||||
@ -1,46 +1,95 @@
|
||||
# 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
|
||||
add support for:
|
||||
|
||||
- 2D Medical Image Viewing (cornerstone ext.)
|
||||
- Structured Reports as HTML (dicom html ext.)
|
||||
- Encapsulated PDFs as PDFs (dicom pdf ext.)
|
||||
- Structured Reports as SR (DICOM SR ext.)
|
||||
- Structured Reports as HTML (DICOM html ext.)
|
||||
- Encapsulated PDFs as PDFs (DICOM pdf ext.)
|
||||
- Whole Slide Microscopy Viewing (whole slide ext.)
|
||||
- etc.
|
||||
|
||||
The general pattern is, the [`sopClassHandlerModule`](#) helps us determine
|
||||
which Viewport Component a set of `sopClassUIDs` should default to. The Viewport
|
||||
Component receives props containing a display set it should know how to render.
|
||||
|
||||
## Viewport Component Props
|
||||
|
||||
Each `ViewportComponent` will receive the following props:
|
||||
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
|
||||
<viewportComponent
|
||||
viewportData="{viewportData}"
|
||||
viewportIndex="{viewportIndex}"
|
||||
children="{[children]}"
|
||||
/>
|
||||
```JSX
|
||||
// displaySet, viewportIndex, dataSource
|
||||
const getViewportModule = () => {
|
||||
const wrappedViewport = props => {
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||

|
||||
### `@ohif/viewer`
|
||||
|
||||
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||
component is used depends on:
|
||||
|
||||
- Hanging Protocols
|
||||
- The Layout Configuration
|
||||
- Registered SopClassHandlers
|
||||
- The SopClassUID for visible/selected datasets
|
||||
|
||||

|
||||
|
||||

|
||||
<center><i>An example of three cornerstone Viewports</i></center>
|
||||
|
||||
@ -30,27 +30,4 @@ You can read more about this particular strategy in our
|
||||
|
||||
## Commonjs Bundle (Packaged Script)
|
||||
|
||||
The [@ohif/viewer][viewer-npm] package is built with WebPack to provide a React
|
||||
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 -->
|
||||
We are not supporting `Commonjs` bundling inside `OHIF-v3`.
|
||||
|
||||
52
docs/latest/managers/_managers.md
Normal 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>
|
||||
179
docs/latest/managers/commands.md
Normal 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');
|
||||
```
|
||||