Introduction
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.
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 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
Diagram showing how extensions are configured and accessed.
Extension Skeleton
An extension is a plain JavaScript object that has id
and version
properties, and one or
more modules and/or lifecycle hooks.
// prettier-ignore
export default {
/**
* Required properties. Should be a unique value across all extensions.
*/
id,
// Lifecyle
preRegistration() { /* */ },
onModeEnter() { /* */ },
onModeExit() { /* */ },
// Modules
getLayoutTemplateModule() { /* */ },
getDataSourcesModule() { /* */ },
getSopClassHandlerModule() { /* */ },
getPanelModule() { /* */ },
getViewportModule() { /* */ },
getCommandsModule() { /* */ },
getContextModule() { /* */ },
getToolbarModule() { /* */ },
getHangingProtocolModule() { /* */ },
getUtilityModule() { /* */ },
}
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
repository, in
the top level extensions/
directory.
Extension | Description | Modules |
---|---|---|
default | Default extension provides default viewer layout, a study/series browser, and a datasource that maps to a DICOMWeb compliant backend | commandsModule, ContextModule, DataSourceModule, HangingProtocolModule, LayoutTemplateModule, PanelModule, SOPClassHandlerModule, ToolbarModule |
cornerstone | Provides 2d and 3d rendering functionalities | ViewportModule, CommandsModule, UtilityModule |
dicom-pdf | Renders PDFs for a specific SopClassUID. | Viewport, SopClassHandler |
dicom-video | Renders DICOM Video files. | Viewport, SopClassHandler |
cornerstone-dicom-sr | Maintained extensions for cornerstone and visualization of DICOM Structured Reports | ViewportModule, CommandsModule, SOPClassHandlerModule |
measurement-tracking | Tracking measurements in the measurement panel | ContextModule,PanelModule,ViewportModule,CommandsModule |
Registering of Extensions
viewer
starts by registering all the extensions specified inside the
pluginConfig.json
, by default we register all extensions in the repo.
// Simplified version of the `pluginConfig.json` file
{
"extensions": [
{
"packageName": "@ohif/extension-cornerstone",
"version": "3.0.0"
},
{
"packageName": "@ohif/extension-measurement-tracking",
"version": "3.0.0"
},
// ...
],
"modes": [
{
"packageName": "@ohif/mode-longitudinal",
"version": "0.0.1"
}
]
}
You SHOULD NOT directly register extensions in the pluginConfig.json
file.
Use the provided cli
to add/remove/install/uninstall extensions. Read more here
The final registration and import of the extensions happen inside a non-tracked file pluginImport.js
(this file is also for internal use only).
After an extension gets registered withing the viewer
,
each module defined by the extension becomes available to the modes
via the ExtensionManager
by requesting it via its id.
Read more about Extension Manager
Lifecycle Hooks
Currently, there are three lifecycle hook for extensions:
preRegistration
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 hook, it is called before any modules are registered in the
ExtensionManager
. It's most commonly used to wire up extensions to
services and commands, and to
bootstrap 3rd party libraries.
onModeEnter
: 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
: 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 cache data for re-use later, but since it
isn't known which mode will be entered next, the state after exiting should be
clean, that is, the same as the state on a clean start. This is called BEFORE
service clean up, and after mode specific onModeExit handling.
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.
Types | Description |
---|---|
LayoutTemplate (NEW) | Control Layout of a route |
DataSource (NEW) | Control the mapping from DICOM metadata to OHIF-metadata |
SOPClassHandler | Determines how retrieved study data is split into "DisplaySets" |
Panel | Adds left or right hand side panels |
Viewport | Adds a component responsible for rendering a "DisplaySet" |
Commands | Adds named commands, scoped to a context, to the CommandsManager |
Toolbar | Adds buttons or custom components to the toolbar |
Context | Shared state for a workflow or set of extension module definitions |
HangingProtocol | Adds hanging protocol rules |
Utility | Expose utility functions to the outside of extensions |
Contexts
The @ohif/viewer
tracks "active contexts" that extensions can use to scope
their functionality. Some example contexts being:
- Route:
ROUTE:VIEWER
,ROUTE:STUDY_LIST
- Active Viewport:
ACTIVE_VIEWPORT:CORNERSTONE
,ACTIVE_VIEWPORT:VTK
An extension module can use these to say "Only show this Toolbar Button if the active viewport is a Cornerstone viewport." This helps us use the appropriate UI and behaviors depending on the current contexts.
For example, if we have hotkey that "rotates the active viewport", each Viewport
module that supports this behavior can add a command with the same name, scoped
to the appropriate context. When the command
is fired, the "active contexts"
are used to determine the appropriate implementation of the rotation behavior.