chore(version): move to the next beta version [BUMP BETA] (#4939)

This commit is contained in:
Alireza authored and GitHub committed 2025-04-09 23:25:30 -04:00
1 parent e1e9b5ccb1
commit e37168094a
374 files changed
+6978 -1721

No files matched your search

@@ -0,0 +1,4 @@
{
"label": "Platform",
"position": 6
}
@@ -0,0 +1,48 @@
---
sidebar_position: 2
---
# Browser Support
The browsers that we support are specified in the `.browserlistrc` file located
in the `platform/app` project. While we leverage the latest language features
when writing code, we rely on `babel` to _transpile_ our code so that it can run
in the browsers that we support.
## In Practice
The OHIF Viewer is capable of _running_ on:
- IE 11
- FireFox
- Chrome
- Safari
- Edge
However, we do not have the resources to adequately test and maintain bug free
functionality across all of these. In order to push web based medical imaging
forward, we focus our development efforts on recent version of modern evergreen
browsers.
Our support of older browsers equates to our willingness to review PRs for bug
fixes, and target their minimum JS support whenever possible.
### Polyfills
> A polyfill, or polyfiller, is a piece of code (or plugin) that provides the
> technology that you, the developer, expect the browser to provide natively.
An example of a polyfill is that you expect `Array.prototype.filter` to exist,
but for some reason, the browser that's being used has not implemented that
language feature yet. Our earlier transpilation will rectify _syntax_
discrepancies, but unimplemented features require a "temporary" implementation.
That's where polyfills step in.
We previously used polyfill io, but due to a security vulnerability in the library, it's necessary to switch to alternative services.
<!--
Links
-->
<!-- prettier-ignore-start -->
[core-js]: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
<!-- prettier-ignore-end -->
@@ -0,0 +1,28 @@
---
sidebar_position: 3
sidebar_label: Environment Variables
---
# Environment Variables
There are a number of environment variables we use at build time to influence the output application's behavior.
```bash
# Application
NODE_ENV=< production | development >
DEBUG=< true | false >
APP_CONFIG=< relative path to application configuration file >
PUBLIC_URL=< relative path to application root - default / >
ROUTER_BASENAME=< relative path to application root - default PUBLIC_URL >
VERSION_NUMBER=<Set by CircleCI>
BUILD_NUM=<Set by CircleCI>
# i18n
USE_LOCIZE=<false>
LOCIZE_PROJECTID=<ProjectID to pull translations for>
LOCIZE_API_KEY=<To enable Locize live editing of translations>
```
## Setting Environment Variables
- `npx cross-env`
- `.env` files
- env variables on build machine, or for terminal session
@@ -0,0 +1,4 @@
{
"label": "Extensions",
"position": 9
}
@@ -0,0 +1,55 @@
---
sidebar_position: 4
sidebar_label: Extension Manager
---
# Extension Manager
## Overview
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:
- `setActiveDataSource` - Sets the active data source for the application
- `getDataSources` - Returns the registered data sources
- `getActiveDataSource` - Returns the currently active data source
- `getModuleEntry` - Returns the module entry by the give id.
## Accessing Modules
We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on
the provided IDs in the mode's configuration.
For instance:
`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")`
accesses the `seriesList` panel from `panelModule` of the
`@ohif/extension-measurement-tracking` extension.
```js
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,
};
};
```
@@ -0,0 +1,345 @@
---
sidebar_position: 1
sidebar_label: Introduction
---
# 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](#maintained-extensions)
**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> -->
## Extension Skeleton
An extension is a plain JavaScript object that has `id` and `version` properties, and one or
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
```js
// prettier-ignore
export default {
/**
* Required properties. Should be a unique value across all extensions.
*/
id,
// Lifecycle
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`][viewers-repo] repository, in
the top level [`extensions/`][ext-source] directory.
<table>
<thead>
<tr>
<th>Extension</th>
<th>Description</th>
<th>Modules</th>
</tr>
</thead>
<tbody>
<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>
<tr>
<td>
<a href="">
cornerstone
</a>
</td>
<td>
Provides 2d and 3d rendering functionalities
</td>
<td>ViewportModule, CommandsModule, UtilityModule</td>
</tr>
<tr>
<td>
<a href="">dicom-pdf</a>
</td>
<td>
Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>.
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<tr>
<td>
<a href="">dicom-video</a>
</td>
<td>
Renders DICOM Video files.
</td>
<td>Viewport, SopClassHandler</td>
</tr>
<tr>
<td>
<a href="">cornerstone-dicom-sr</a>
</td>
<td>
Maintained extensions for cornerstone and visualization of DICOM Structured Reports
</td>
<td>ViewportModule, CommandsModule, SOPClassHandlerModule</td>
</tr>
<tr>
<td>
<a href="">measurement-tracking</a>
</td>
<td>
Tracking measurements in the measurement panel
</td>
<td> ContextModule,PanelModule,ViewportModule,CommandsModule</td>
</tr>
</tbody>
</table>
## Registering of Extensions
`viewer` starts by registering all the extensions specified inside the
`pluginConfig.json`, by default we register all extensions in the repo.
```js title=platform/app/pluginConfig.json
// Simplified version of the `pluginConfig.json` file
{
"extensions": [
{
"packageName": "@ohif/extension-cornerstone",
"version": "3.4.0"
},
{
"packageName": "@ohif/extension-measurement-tracking",
"version": "3.4.0"
},
// ...
],
"modes": [
{
"packageName": "@ohif/mode-longitudinal",
"version": "3.4.0"
}
]
}
```
:::note Important
You SHOULD NOT directly register extensions in the `pluginConfig.json` file.
Use the provided `cli` to add/remove/install/uninstall extensions. Read more [here](../../development/ohif-cli.md)
:::
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 within the `viewer`,
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)
## Lifecycle Hooks
Currently, there are three lifecycle hook for extensions:
[`preRegistration`](./lifecycle/#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/#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](./../services/index.md) and [commands](./modules/commands.md), and to
bootstrap 3rd party libraries.
[`onModeEnter`](./lifecycle#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`](./lifecycle#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 reuse 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.
<table>
<thead>
<tr>
<th align="left" width="30%">
Types
</th>
<th align="left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">
<a href="./modules/layout-template">
LayoutTemplate
</a>
</td>
<td align="left">Control Layout of a route</td>
</tr>
<tr>
<td align="left">
<a href="./modules/data-source">
DataSource
</a>
</td>
<td align="left">Control the mapping from DICOM metadata to OHIF-metadata</td>
</tr>
<tr>
<td align="left">
<a href="./modules/sop-class-handler">
SOPClassHandler
</a>
</td>
<td align="left">Determines how retrieved study data is split into "DisplaySets"</td>
</tr>
<tr>
<td align="left">
<a href="./modules/panel">
Panel
</a>
</td>
<td align="left">Adds left or right hand side panels</td>
</tr>
<tr>
<td align="left">
<a href="./modules/viewport">
Viewport
</a>
</td>
<td align="left">Adds a component responsible for rendering a "DisplaySet"</td>
</tr>
<tr>
<td align="left">
<a href="./modules/commands">
Commands
</a>
</td>
<td align="left">Adds named commands, scoped to a context, to the CommandsManager</td>
</tr>
<tr>
<td align="left">
<a href="./modules/toolbar">
Toolbar
</a>
</td>
<td align="left">Adds buttons or custom components to the toolbar</td>
</tr>
<tr>
<td align="left">
<a href="./modules/contextModule">
Context
</a>
</td>
<td align="left">Shared state for a workflow or set of extension module definitions</td>
</tr>
<tr>
<td align="left">
<a href="./modules/hpModule">
HangingProtocol
</a>
</td>
<td align="left">Adds hanging protocol rules</td>
</tr>
<tr>
<td align="left">
<a href="./modules/hpModule">
Utility
</a>
</td>
<td align="left">Expose utility functions to the outside of extensions</td>
</tr>
</tbody>
</table>
<span style={{"textAlign": 'center', 'fontStyle': 'italic'}}>Tbl. Module types
with abridged descriptions and examples. Each module links to a dedicated
documentation page.</span>
### Contexts
The `@ohif/app` 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.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[viewers-repo]: https://github.com/OHIF/Viewers
[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
<!-- prettier-ignore-end -->
@@ -0,0 +1,12 @@
---
sidebar_position: 5
sidebar_label: Installation
---
# Extension: Installation
OHIF-v3 provides the ability to utilize external extensions.
You can use ohif `cli` tool to install both local and publicly published
extensions on NPM. You can read more [here](../../development/ohif-cli.md)
@@ -0,0 +1,128 @@
---
sidebar_position: 3
sidebar_label: Lifecycle Hooks
---
# Extensions: Lifecycle Hooks
## Overview
Extensions can implement specific lifecycle methods.
- preRegistration
- onModeEnter
- onModeExit
## preRegistration
If an extension defines the `preRegistration` lifecycle hook, it is called
before any modules are registered in the `ExtensionManager`. This hook is an
`async` function that can be used to perform:
- initialize 3rd party libraries
- register event listeners
- add or call services
- add or call commands
The `preRegistration` hook receives an object containing the
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
`configuration` that was provided with the extension at time of registration.
Example `preRegistration` 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(servicesManager) {
return {
name: 'MyNewService',
create: ({ configuration = {} }) => {
return new MyNewService(servicesManager);
},
};
}
```
and
```js
import MyNewService from './MyNewService'
export default {
id,
/**
* @param {object} params
* @param {object} params.configuration
* @param {ServicesManager} params.servicesManager
* @param {CommandsManager} params.commandsManager
* @returns void
*/
async preRegistration({ servicesManager, commandsManager, configuration }) {
console.log('Wiring up important stuff.');
window.importantStuff = () => {
console.log(configuration);
};
console.log('Important stuff has been wired.');
window.importantStuff();
// Registering new services
servicesManager.registerService(MyNewService(servicesManager));
},
},
};
```
## 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: '@ohif/extension-cornerstone-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;
});
},
};
```
## 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 }) {
myCacheService.purge();
},
};
```
@@ -0,0 +1,4 @@
{
"label": "Modules",
"position": 3
}
@@ -0,0 +1,121 @@
---
sidebar_position: 2
sidebar_label: Commands
---
# Module: Commands
## 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
specific [contexts](./../index.md#contexts). Commands have several unique
characteristics that make them tremendously powerful:
- Multiple implementations for the same command can be defined
- Only the correct command's implementation will be run, dependent on the
application's "context"
- Commands are used by hotkeys, toolbar buttons and render settings
Here is a simple example commands module:
```js
const getCommandsModule = () => ({
definitions: {
exampleActionDef: {
commandFn: ({ param1 }) => {
console.log(`param1's value is: ${param1}`);
},
// 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 (`exampleActionDef` below) and a
`commandFn`. The command name is used to call the command, and the `commandFn`
is the "command" that is actioned.
```js
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`. |
| `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
**If there are multiple valid commands for the application's active contexts**
- What happens: all commands are run
- When to use: A `clearData` command that cleans up state for multiple
extensions
**If no commands are valid for the application's active contexts**
- What happens: a warning is printed to the console
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
current viewport (PDF or HTML)
## `CommandsManager` Public API
If you would like to run a command in the consuming app or an extension, you can
use `CommandsManager.runCommand(commandName, options = {}, contextName)`
```js
// Returns all commands for a given context
commandsManager.getContext('string');
// 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']);
```
The `ExtensionManager` handles registering commands and creating contexts, so
most consumer's won't need these methods. If you find yourself using these, ask
yourself "why can't I register these commands via an extension?"
```js
// Used by the `ExtensionManager` to register new commands
commandsManager.registerCommand('context', 'name', commandDefinition);
// Creates a new context; clears the context if it already exists
commandsManager.createContext('string');
```
### Contexts
It is up to the consuming application to define what contexts are possible, and
which ones are currently active. As extensions depend heavily on these, we will
likely publish guidance around creating contexts, and ways to override extension
defined contexts in the near future. If you would like to discuss potential
changes to how contexts work, please don't hesitate to create a new GitHub
issue.
[Some additional information on Contexts can be found here.](./../index.md#contexts)
@@ -0,0 +1,30 @@
---
sidebar_position: 9
sidebar_label: Context
---
# Module: Context
## 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,
},
];
```
@@ -0,0 +1,212 @@
---
sidebar_position: 3
sidebar_label: Data Source
---
# Module: Data Source
## Overview
We have built couple of methods 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';
import { createDicomJSONApi } from './DicomJSONDataSource/index';
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.
```js title="platform/core/src/DataSources/IWebApiDataSource.js"
function create({
initialize,
query,
retrieve,
store,
reject,
parseRouteParams,
deleteStudyMetadataPromise,
getImageIdsForDisplaySet,
getImageIdsForInstance,
}) {
/* */
}
```
You can take a look at `dicomweb` data source implementation to get an idea
`extensions/default/src/DicomWebDataSource/index.js` but here here are some
important api endpoints that you need to implement:
- `initialize`: This method is called when the data source is first created in the mode.tsx, it is used to initialize the data source and set the configuration. For instance, `dicomwebDatasource` uses this method to grab the StudyInstanceUID from the URL and set it as the active study, as opposed to `dicomJSONDatasource` which uses url in the browser to fetch the data and store it in a cache
- `query.studies.search`: This is used in the study panel on the left to fetch the prior studies for the same MRN which is then used to display on the `All` tab. it is also used in the Worklist to show all the studies from the server.
- `query.series.search`: This is used to fetch the series information for a given study that is expanded in the Worklist.
- `retrieve.bulkDataURI`: used to render RTSTUCTURESET in the viewport. It is an object that contains `enabled` as property and other options that are specific to the data source.
- `retrieve.series.metadata`: It is a crucial end point that is used to fetch series level metadata which for hanging displaySets and displaySet creation.
- `store.dicom`: If you don't need store functionality, you can skip this method. This is used to store the data in the backend.
## Static WADO Client
If the configuration for the data source has the value staticWado set, then it
is assumed that queries for the studies return a super-set of the studies, as it
is assumed to be returning a static list. The StaticWadoClient performs the
search functionality manually, by interpreting the query parameters and then
applying them to the returned response. This functionality may be useful for
other types of DICOMweb back ends, where they are capable of performing queries,
but don't allow for querying certain types of fields. However, that only works
as long as the size of the studies list isn't too large that client side
selection isn't too expensive.
## 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.
## Adding a Data Source Outside a Module
A data source can be added outside a module via `ExtensionManager.addDataSource`.
The following snippet of code demonstrates how `addDataSource` can be used to add
a new DICOMWeb data source for the Google Cloud Healthcare API and set it as the
active data source.
```js
extensionManager.addDataSource({
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
sourceName: 'google',
configuration: {
friendlyName: 'dcmjs DICOMWeb Server',
name: 'GCP',
wadoUriRoot:
'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb',
qidoRoot:
'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb',
wadoRoot:
'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
supportsWildcard: false,
dicomUploadEnabled: true,
omitQuotationForMultipartRequest: true,
},
{activate:true}
});
```
## Updating a Data Source's Configuration
An existing data source can have its configuration updated using the
`ExtensionManager.updateDataSourceConfiguration` method. The following snippet of
code demonstrates how `updateDataSourceConfiguration` can be use to update the
configuration of an existing DICOMWeb data source (named `dicomweb`) with the
configuration for a Google Cloud Healthcare API data source.
```js
extensionManager.updateDataSourceConfiguration( "dicomweb",
{
name: 'GCP',
wadoUriRoot:
'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb',
qidoRoot:
'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb',
wadoRoot:
'https://healthcare.googleapis.com/v1/projects/ohif-cloud-healthcare/locations/us-east4/datasets/ohif-qa-dataset/dicomStores/ohif-qa-2/dicomWeb',
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
supportsFuzzyMatching: true,
supportsWildcard: false,
dicomUploadEnabled: true,
omitQuotationForMultipartRequest: true,
},
);
```
## Merge Data Source
The built-in merge data source is a useful tool for combining results from multiple data sources.
Currently, this data source only supports merging at the series level. This means that series from data source 'A'
and series from data source 'B' will be retrieved under the same study. If the same series exists in both data sources,
the first series arrived is the one that gets stored, and any other conflicting series will be ignored.
The merge data source is particularly useful when dealing with derived data that is generated and stored in different servers.
For example, it can be used to retrieve annotation series from one data source and input data (images) from another data source.
A default data source can be defined as shown below. This allows defining which of the servers should be the
fallback server in case something goes wrong.
Configuration Example:
```js
window.config = {
...
dataSources: [
{
sourceName: 'merge',
namespace: '@ohif/extension-default.dataSourcesModule.merge',
configuration: {
name: 'merge',
friendlyName: 'Merge dicomweb-1 and dicomweb-2 data at the series level',
seriesMerge: {
dataSourceNames: ['dicomweb-1', 'dicomweb-2'],
defaultDataSourceName: 'dicomweb-1'
},
},
},
{
sourceName: 'dicomweb-1',
...
},
{
sourceName: 'dicomweb-2',
...
},
],
};
```
@@ -0,0 +1,624 @@
---
sidebar_position: 8
sidebar_label: Hanging Protocol
---
# Module: Hanging Protocol
## Overview
[Hanging protocols](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf) are an essential part of any radiology viewer.
OHIF uses Hanging Protocols to handle the arrangement of the images in the viewport. In
short, the registered protocols will get matched with the DisplaySets that are
available. Each protocol gets a score, and they are ranked. The
winning protocol (highest score) gets applied and its settings run for the viewports
to be arranged.
In `OHIF-v3` hanging protocols you can:
- Define what layout the viewport should starts with (e.g., 2x2 layout)
- Specify the type of the viewport and its orientation (e.g., stack, volume with Sagittal view)
- Define which displaySets gets displayed in which viewport of the layout (e.g,. displaySet that has modality of 'CT' and 'SeriesDescription' of 'Coronary Arteries' gets displayed in the first viewport of the layout)
- Apply certain initial viewport settings (e.g., inverting the contrast, jumping to a specific slice, etc.)
- Add specific synchronization rules for the viewports (e.g., synchronize the zoom of the viewports of the index 1, 2 OR synchronize the VOI of the viewports of the index 2, 3)
Using `hangingProtocolModule` you can provide/register the protocols for OHIF to
utilize.
Here is an example protocol which if used will hang a 1x3 layout with the first viewport showing a CT image, the second viewport showing a PT image and the third viewport showing their fusion, all in Sagittal orientations to achieve a view of
![](../../../assets/img/hangingProtocolExample.png)
```js
const oneByThreeProtocol = {
id: 'oneByThreeProtocol',
locked: true,
name: 'Default',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2022-10-04T19:22:08.894Z',
availableTo: {},
editableBy: {},
imageLoadStrategy: 'interleaveTopToBottom',
protocolMatchingRules: [
{
attribute: 'ModalitiesInStudy',
constraint: {
contains: ['CT', 'PT'],
},
},
],
displaySetSelectors: {
ctDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
ptDisplaySet: {
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: 'PT',
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'SeriesDescription',
constraint: {
contains: 'Corrected',
},
},
],
},
},
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
name: 'default',
viewportStructure: {
layoutType: 'grid',
properties: {
rows: 1,
columns: 3,
},
},
viewports: [
{
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'ptAXIAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
},
displaySets: [
{
id: 'ptDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'fusionSAGITTAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'ctWLSync',
source: false,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
{
options: {
colormap: 'hsv',
voi: {
windowWidth: 5,
windowCenter: 2.5,
},
},
id: 'ptDisplaySet',
},
],
},
],
createdDate: '2021-02-23T18:32:42.850Z',
},
],
numberOfPriorsReferenced: -1,
};
function getHangingProtocolModule() {
return [
{
id: 'oneByThreeProtocol',
protocol: oneByThreeProtocol,
},
];
}
```
## Skeleton of a Protocol
The skeleton of a hanging protocol is as follows:
### Id
unique identifier for the protocol, this id can be used inside mode configuration
to specify which protocol should be used for a specific mode. A mode can
request a protocol by its id (which makes OHIF to apply the protocol without
matching), or provides an array of ids which will
make the ProtocolEngine to choose the best matching protocol (based on
protocolMatching rules, which is next section).
### imageLoadStrategy
The image load strategy specifies a function (by name) containing logic to re-order
the image load requests. This allows loading images viewed earlier to be done
sooner than those loaded later. The available strategies are:
* interleaveTopToBottom to start at the top and work towards the bottom, for all series being loaded
* interleaveCenter is like top to bottom but starts at the center
* nth is a strategy that loads every nth instance, starting with the center
and end points, and then filling in progressively all along the image. This results in partial
image view very quickly.
### protocolMatchingRules
A list of criteria for the protocol along with the provided points for ranking.
- `weight`: weight for the matching rule. Eventually, all the registered
protocols get sorted based on the weights, and the winning protocol gets
applied to the viewer.
- `attribute`: tag that needs to be matched against. This can be either
Study-level metadata or a custom attribute such as "StudyInstanceUID",
"StudyDescription", "ModalitiesInStudy", "NumberOfStudyRelatedSeries", "NumberOfSeriesRelatedInstances"
In addition to these tags, you can also use a custom attribute that you have registered before.
We will learn more about this later.
- `from`: Indicates the source of the attribute. This allows getting values
from other objects such as the `prior` instance object instead of from the
current one.
- `constraint`: the constraint that needs to be satisfied for the attribute. It accepts a `validator` which can be
[`equals`, `doesNotEqual`, `contains`, `doesNotContain`, `startsWith`, `endsWidth`]
| Rule | Single Value | Array Value | Example |
|------|--------------|-------------|---------|
| equals | &equals;&equals;&equals; | All members are &equals;&equals;&equals; in same order | value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'abc' (Fail)<br/>&equals; ['abc'] (Fail)<br/>&equals; ['abc', 'def', 'GHI'] (Valid)<br/>&equals; ['abc', 'GHI', 'def'] (Fail)<br/>&equals; ['abc', 'def'] (Fail)<br/><br/>value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Attenuation Corrected' (Valid)<br/>&equals; 'Attenuation' (Fail)<br/><br/>value &equals; ['Attenuation Corrected']<br/>testValue &equals; ['Attenuation Corrected'] (Valid)<br/>&equals; 'Attenuation Corrected' (Valid)<br/>&equals; 'Attenuation' (Fail) |
| doesNotEqual | !&equals;&equals; | Any member is !&equals;&equals; for the array, either in value, order, or length | value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'abc' (Valid)<br/>&equals; ['abc'] (Valid)<br/>&equals; ['abc', 'def', 'GHI'] (Fail)<br/>&equals; ['abc', 'GHI', 'def'] (Valid)<br/>&equals; ['abc', 'def'] (Valid)<br/><br/>value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Attenuation Corrected' (Fail)<br/>&equals; 'Attenuation' (Valid)<br/><br/>value &equals; ['Attenuation Corrected']<br/>testValue &equals; ['Attenuation Corrected'] (Fail)<br/>&equals; 'Attenuation Corrected' (Fail)<br/>&equals; 'Attenuation' (Fail) |
| includes | Not allowed | Value is equal to one of the values of the array | value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; ['abc'] (Valid)<br/>&equals; 'abc' (Fail)<br/>&equals; ['abc'] (Fail)<br/>&equals; 'dog' (Fail)<br/>&equals; ['att', 'abc'] (Valid)<br/>&equals; ['abc', 'def', 'dog'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail)<br/><br/>value &equals; 'Attenuation Corrected'<br/>testValue &equals; ['Attenuation Corrected', 'Corrected'] (Valid)<br/>&equals; ['Attenuation', 'Corrected'] (Fail)<br/><br/>value &equals; ['Attenuation Corrected']<br/>testValue &equals; 'Attenuation Corrected' (Fail)<br/>&equals; ['Attenuation Corrected', 'Corrected'] (Valid)<br/>&equals; ['Attenuation', 'Corrected'] (Fail) |
| doesNotInclude | Not allowed | Value is not in one of the values of the array | value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'Corr' (Valid)<br/>&equals; 'abc' (Fail)<br/>&equals; ['att', 'cor'] (Valid)<br/>&equals; ['abc', 'def', 'dog'] (Fail)<br/><br/>value &equals; 'Attenuation Corrected'<br/>testValue &equals; ['Attenuation Corrected', 'Corrected'] (Fail)<br/>&equals; ['Attenuation', 'Corrected'] (Valid)<br/><br/>value &equals; ['Attenuation Corrected']<br/>testValue &equals; 'Attenuation' (Fail)<br/>&equals; ['Attenuation Corrected', 'Corrected'] (Fail)<br/>&equals; ['Attenuation', 'Corrected'] (Valid) |
| containsI | String containment (case insensitive) | String containment (case insensitive) is OK for one of the rule values | value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Corr' (Valid)<br/>&equals; 'corr' (Valid)<br/>&equals; ['att', 'cor'] (Valid)<br/>&equals; ['Att', 'Wall'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail)<br/><br/>value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'def' (Valid)<br/>&equals; 'dog' (Fail)<br/>&equals; ['gh', 'de'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail) |
| contains | String containment (case sensitive) | String containment (case sensitive) is OK for one of the rule values | value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Corr' (Valid)<br/>&equals; 'corr' (Fail)<br/>&equals; ['att', 'cor'] (Fail)<br/>&equals; ['Att', 'Wall'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail)<br/><br/>value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'def' (Valid)<br/>&equals; 'dog' (Fail)<br/>&equals; ['cat', 'de'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail) |
| doesNotContain | String containment is false | String containment is false for all values of the array | value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Corr' (Fail)<br/>&equals; 'corr' (Valid)<br/>&equals; ['att', 'cor'] (Valid)<br/>&equals; ['Att', 'Wall'] (Fail)<br/>&equals; ['cat', 'dog'] (Valid)<br/><br/>value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'def' (Fail)<br/>&equals; 'dog' (Valid)<br/>&equals; ['cat', 'de'] (Fail)<br/>&equals; ['cat', 'dog'] (Valid) |
| doesNotContainI | String containment is false (case insensitive) | String containment (case insensitive) is false for all values of the array | value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Corr' (Fail)<br/>&equals; 'corr' (Fail)<br/>&equals; ['att', 'cor'] (Fail)<br/>&equals; ['Att', 'Wall'] (Fail)<br/>&equals; ['cat', 'dog'] (Valid)<br/><br/>value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'DEF' (Fail)<br/>&equals; 'dog' (Valid)<br/>&equals; ['cat', 'gh'] (Fail)<br/>&equals; ['cat', 'dog'] (Valid) |
| startsWith | Value begins with characters | Starts with one of the values of the array | value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'Corr' (Fail)<br/>&equals; 'Att' (Fail)<br/>&equals; ['cat', 'dog', 'Att'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail)<br/><br/>value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'deg' (Valid)<br/>&equals; ['cat', 'GH'] (Valid)<br/>&equals; ['cat', 'gh'] (Fail)<br/>&equals; ['cat', 'dog'] (Fail) |
| endsWith | Value ends with characters | ends with one of the value of the array | value &equals; 'Attenuation Corrected'<br/>testValue &equals; 'TED' (Fail)<br/>&equals; 'ted' (Valid)<br/>&equals; ['cat', 'dog', 'ted'] (Valid)<br/>&equals; ['cat', 'dog'] (Fail)<br/><br/>value &equals; ['abc', 'def', 'GHI']<br/>testValue &equals; 'deg' (Valid)<br/>&equals; ['cat', 'HI'] (Valid)<br/>&equals; ['cat', 'hi'] (Fail)<br/>&equals; ['cat', 'dog'] (Fail) |
| greaterThan | value is &gt;&equals; to rule | Not applicable | value &equals; 30<br/>testValue &equals; 20 (Valid)<br/>&equals; 40 (Fail) |
| lessThan | value is &lt;&equals; to rule | Not applicable | value &equals; 30<br/>testValue &equals; 40 (Valid)<br/>&equals; 20 (Fail) |
| range | Not applicable | 2 value requested (min and max) | value &equals; 50<br/>testValue &equals; [10,60] (Valid)<br/>&equals; [60, 10] (Valid)<br/>&equals; [0, 10] (Fail)<br/>&equals; [70, 80] (Fail)<br/>&equals; 45 (Fail)<br/>&equals; [45] (Fail) |
| notNull | Not Applicable | Not Applicable | No value | |
A sample of the matching rule is above which matches against the study description to be PETCT
```js
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'StudyDescription',
constraint: {
contains: {
value: 'PETCT',
},
},
required: false,
},
```
### `from` attribute (optional)
The `from` attribute allows you to retrieve the attribute to test from another object, such as the previous study, the overall list of studies, or another provided value from a module.
The values provided by OHIF which you can use are:
- `activeStudy`: to use the metadata of the active study to match
- `studies`: to use the metadata of the list of studies (all studies) to match
- `allDisplaySets`: all available display sets
- `displaySets`: if the selector has matched a study, these are the display sets for that study
- `prior`: the metadata of the first study in the list of studies that is not the active study
- `options`: during matching, we also provide an options object with the following information that you can use as the `from` value:
- `studyInstanceUIDsIndex`: the index of the study in the list of studies
- `instance`: the metadata of the instance being matched, which is exactly the displaySet.instance metadata.
### displaySetSelectors (mandatory)
Defines the display sets that the protocol will use for arrangement.
```js
displaySetSelectors: {
ctDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
ptDisplaySet: {
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: 'PT',
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'SeriesDescription',
constraint: {
contains: 'Corrected',
},
},
],
},
}
```
As you see above we have specified two displaysets: 1) ctDisplaySet , 2) ptDisplaySet
The ctDisplaySet will match against all the series that are CT and reconstructable
The ptDisplaySet will match against all the series that are PT and reconstructable.
As you see each selector is composed of an `id` as the key and a set of `seriesMatchingRules` (displaySetMatchingRules) which gives score to the displaySet
based on the matching rules. The displaySet with the highest score will be used for the `id`.
### stages
Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules. Therefore, the `stages` property is an array of objects, each object being one stage.
### viewportStructure
Defines the layout of the viewer. You can define the number of `rows` and `columns`. There should be `rows * columns` number of
viewport configuration in the `viewports` property. Note that order of viewports are rows first then columns.
```js
viewportStructure: {
type: 'grid',
properties: {
rows: 1,
columns: 2,
viewportOptions: [],
},
},
```
In addition to the equal viewport sizes, you can define viewports to span multiple rows or columns.
```js
viewportStructure: {
type: 'grid',
properties: {
rows: 1,
columns: 2,
viewportOptions: [
{
x: 0,
y: 0,
width: 1 / 4,
height: 1,
},
{
x: 1 / 4,
y: 0,
width: 3 / 4,
height: 1,
},
],
},
},
```
### viewports
This field includes the viewports that will get hung on the viewer.
```js
viewports: [
{
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
],
},
// the rest
],
```
As you can see in the hanging protocol we defined three viewports (but only showing one of them right above). Each viewport has two properties:
1. `viewportOptions`: defines the viewport properties such as
- `viewportId`: unique identifier for the viewport (optional)
- `viewportType`: type of the viewport (optional - options: stack, volume - default is stack
- `background`: background color of the viewport (optional)
- `orientation`: orientation of the viewport (optional - if not defined for volume -> acquisition axis)
- `toolGroupId`: tool group that will be used for the viewport (optional)
- `initialImageOptions`: initial image options (optional - can be specific imageIndex number or preset (first, middle, last))
- `syncGroups`: sync groups for the viewport (optional)
-The `displayArea` parameter refers to the designated area within the viewport where a specific portion of the image can be displayed. This parameter is optional and allows you to choose the location of the image within the viewport. For example, in mammography images, you can display the left breast on the left side of the viewport and the right breast on the right side, with the chest wall positioned in the middle. To understand how to define the display area, you can refer to the live example provided by CornerstoneJS [here](https://www.cornerstonejs.org/live-examples/programaticpanzoom).
2. `displaySets`: defines the display sets that are displayed on a viewport. It is an array of objects, each object being one display set.
- `id`: id of the display set (required)
- `options` (optional): options for the display set
- voi: windowing options for the display set (optional: windowWidth, windowCenter)
- voiInverted: whether the VOI is inverted or not (optional)
- colormap: colormap for the display set (optional, it is an object with `{ name }` and optional extra `opacity` property)
- displayPreset: display preset for the display set (optional, used for 3D volume rendering. e.g., 'CT-Bone')
### Custom attribute
For any matching rules you can specify a custom attribute too. For instance,
if you have a timepoint attribute in for each of your studies, you can use that in the matching rules.
```js
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'timepoint',
constraint: {
equals: {
value: 'baseline',
},
},
required: false,
},
```
and then you need to register a callback in the HangingProtocolService to get the value for the attribute.
```js
HangingProtocolService.addCustomAttribute(
'timepoint', // attributeId
'addCustomAttribute', // attributeName
study => { // callback that returns the value for the attribute
const timePoint = fetchFromMyCustomBackend(study.studyInstanceUid);
return timePoint;
}
);
```
## Matching on Prior Study with UID
Often it is desired to match a new study to a prior study (e.g., follow up on
a surgery). Since the hanging protocols run on displaySets we need to have a
way to let OHIF knows that it needs to load the prior study as well. This can
be done by specifying both StudyInstanceUIDs in the URL. The additional studies
are then accessible to the hanging protocol. Below we are
running OHIF with two studies, and a comparison hanging protocol available by
default.
```bash
https://viewer-dev.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095258.1&hangingprotocolId=@ohif/hpCompare
```
The `&hangingProtocolId` option forces the specific hanging protocol to be
applied, but the mode can also add the hanging protocols to the default set,
and then the best matching hanging protocol will be applied by the run method.
To match any other studies, it is required to enable the prior matching rules
capability using:
```javascript
// Indicate number of priors used - 0 means any number, -1 means none.
numberOfPriorsReferenced: 1,
```
The matching rule that allows the hanging protocol to be runnable is:
```javascript
protocolMatchingRules: [
{
id: 'Two Studies',
weight: 1000,
// This will generate 1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1
// since that is study instance UID in the prior from instance.
attribute: 'StudyInstanceUID',
// The 'from' attribute says where to get the 'attribute' value from. In this case
// prior means the second study in the study list.
from: 'prior',
required: true,
constraint: {
notNull: true,
},
},
],
```
The display set selector selecting the specific study to display is included
in the studyMatchingRules. Note that this rule will cause ONLY the second study
to be matched, so it won't attempt to match anything in other studies.
Additional series level criteria, such as modality rules must be included at the
`seriesMatchingRules`.
```javascript
studyMatchingRules: [
{
// The priorInstance is a study counter that indicates what position this study is in
// and the value comes from the options parameter.
attribute: 'studyInstanceUIDsIndex',
from: 'options',
required: true,
constraint: {
equals: { value: 1 },
},
},
],
```
## Callbacks
Hanging protocols in `OHIF-v3` provide the flexibility to define various callbacks that allow you to customize the behavior of your viewer when specific events occur during protocol execution. These callbacks are defined in the `ProtocolNotifications` type and can be added to your hanging protocol configuration.
Each callback is an array of commands or actions that are executed when the event occurs.
```js
[
{
commandName: 'showDownloadViewportModal',
commandOptions: {}
}
]
```
Here, we'll explain the available callbacks and their purposes:
### `onProtocolExit`
The `onProtocolExit` callback is executed after the protocol is exited and the new one is applied. This callback is useful for performing actions or executing commands when switching between hanging protocols.
### `onProtocolEnter`
The `onProtocolEnter` callback is executed after the protocol is entered and applied. You can use this callback to define actions or commands that should run when entering a specific hanging protocol.
### `onLayoutChange`
The `onLayoutChange` callback is executed before the layout change is started. You can use it to apply a specific hanging protocol based on the current layout or other criteria.
### `onViewportDataInitialized`
The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start.
Here is an example of how you can add these callbacks to your hanging protocol configuration:
```javascript
const protocol = {
id: 'myProtocol',
name: 'My Protocol',
// rest of the protocol configuration
callbacks: {
onProtocolExit: [
// Array of commands or actions to execute on protocol exit
],
onProtocolEnter: [
// Array of commands or actions to execute on protocol enter
],
onLayoutChange: [
// Array of commands or actions to execute on layout change
],
onViewportDataInitialized: [
// Array of commands or actions to execute on viewport data initialization
],
},
// protocolMatchingRules
// the rest
};
@@ -0,0 +1,139 @@
---
sidebar_position: 7
sidebar_label: Layout Template
---
# Module: Layout Template
## 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 extension`'s layout template is:
```jsx title="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}
/>
{/* Right SIDEPANELS */}
<SidePanel
side="right"
defaultComponentOpen={rightPanelComponents[0].name}
childComponents={rightPanelComponents}
/>
</div>
</div>
);
}
```
## Overview Video
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/545993263?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
@@ -0,0 +1,163 @@
---
sidebar_position: 6
sidebar_label: Panel
---
# Module: Panel
## Overview
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.
![panel-module-v3](../../../assets/img/panel-module-v3.png)
The `menuOptions`'s `target` key, points to a registered `components`'s `id`. A
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
optionally provide its own `context` value.
The `getPanelModule` receives an object containing the `ExtensionManager`'s
associated `ServicesManager` and `CommandsManager`.
An extension can also trigger to activate/open a panel via the `PanelService` -
either by explicitly calling `PanelService.activatePanel` or triggering panel
activation when some other event fires.
```jsx
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`. The mode configuration also allows for either (or
both) side panels to be closed by default. In the code below, the right panel
is closed by default. The mode can optionally add event triggers to
the `PanelService` that when fired will cause a side panel that was defaulted
closed to open. In the code below, the right side panel, that contains the
`trackedMeasurements` panel, is triggered to open when a measurement is added.
Note that once a default closed side panel has been opened once,
only a `PanelService.EVENTS.ACTIVATE_PANEL` event with `forceActive === true`
will cause it open (again).
```js
const extensionDependencies = {
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'@ohif/extension-measurement-tracking': '^3.0.0',
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
};
const id = 'viewer'
const version = '3.0.0
function modeFactory({ modeConfiguration }) {
let _activatePanelTriggersSubscriptions = [];
return {
id,
routes: [
{
path: 'longitudinal',
layoutTemplate: ({ location, servicesManager }) => {
return {
id,
props: {
leftPanels: [
'@ohif/extension-measurement-tracking.panelModule.seriesList',
],
rightPanels: [
'@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
],
rightPanelClosed: true,
viewports,
},
};
},
},
],
onModeEnter: ({ servicesManager }) => {
const {
measurementService,
panelService,
} = servicesManager.services;
_activatePanelTriggersSubscriptions = [
...panelService.addActivatePanelTriggers('@ohif/extension-measurement-tracking.panelModule.trackedMeasurements', [
{
sourcePubSubService: measurementService,
sourceEvents: [
measurementService.EVENTS.MEASUREMENT_ADDED,
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
],
},
]),
];
},
onModeExit: () => {
_activatePanelTriggersSubscriptions.forEach(sub => sub.unsubscribe());
_activatePanelTriggersSubscriptions = [];
},
extensions: extensionDependencies
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
};
export default mode;
```
:::note
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
For instance we can use
```
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
```
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
:::
@@ -0,0 +1,120 @@
---
sidebar_position: 4
sidebar_label: SOP Class Handler
---
# Module: SOP Class Handler
## Overview
This module defines how a specific DICOM SOP class should be processed to make a list of `DisplaySet`, things which 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 its own components. It instead defines:
- `sopClassUIDs`: an array of string SOP Class UIDs that the
`getDisplaySetFromSeries` method should be applied to.
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
display set
A `displaySet` has the following shape:
```js
return {
Modality: 'MR',
displaySetInstanceUIDD
SeriesDate,
SeriesTime,
SeriesInstanceUID,
StudyInstanceUID,
SeriesNumber,
FrameRate,
SeriesDescription,
isMultiFrame,
numImageFrames,
SOPClassHandlerId,
madeInClient,
}
```
## Example SOP Class Handler Module
```js
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",
};
// 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,
];
function addInstances(instances) {
// Add instances to this display set, and return the display set updated.
}
const makeDisplaySet = (instances) => {
const instance = instances[0];
const imageSet = new ImageSet(instances);
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}`,
addInstances,
});
// Note returns an array now
return [imageSet];
};
getSopClassHandlerModule = () => {
return [
{
name: 'stack,
sopClassUids,
getDisplaySetsFromSeries: makeDisplaySet,
},
];
};
```
### addInstances
In order to allow new SOP instances to be received and added to an existing display
set, the addInstances method can be added to a display set. It is called
on the display set to be updated, and returns it when it has added at least one
of the instances to the display set.
### More examples :
You can find another example for this mapping between raw metadata and displaySet for
`DICOM-SR` extension.
## `@ohif/app` usage
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
[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12
[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6
[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7
[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7
<!-- prettier-ignore-end -->
@@ -0,0 +1,573 @@
---
sidebar_position: 1
sidebar_label: Toolbar
---
# Module: Toolbar
An extension can register a Toolbar Module by defining a `getToolbarModule`
method. `OHIF-v3`'s `default` extension (`"@ohif/extension-default"`) provides the
following toolbar button `uiTypes`:
- `ohif.radioGroup`: which is a simple button that can be clicked
- `ohif.splitButton`: which is a button with a dropdown menu
- `ohif.divider`: which is a simple divider
## Example Toolbar Module
The Toolbar Module should return an array of `objects`. There are currently a
few different variations of definitions, each one is detailed further down.
There are two things that the toolbar module can provide, first
a component, and second evaluators.
### Components
```js
export default function getToolbarModule({ commandsManager, servicesManager }) {
return [
{
name: 'ohif.radioGroup',
defaultComponent: ToolbarButton,
clickHandler: () => {},
},
{
name: 'ohif.splitButton',
defaultComponent: ToolbarSplitButton,
clickHandler: () => {},
},
{
name: 'ohif.layoutSelector',
defaultComponent: ToolbarLayoutSelector,
clickHandler: (evt, clickedBtn, btnSectionName) => {},
},
{
name: 'ohif.toggle',
defaultComponent: ToolbarButton,
clickHandler: () => {},
},
];
}
```
### Custom Components
You can also create your own extension, and add your new custom tool appearance
(e.g., split horizontally 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: () => {},
},
];
}
```
Check out how to assemble the toolbar in the [modes](../../modes/index.md) section.
### Evaluators
Buttons may be equipped with evaluators, which are functions invoked by the toolbarService to assess the button's status. These evaluators are expected to return an object of `{className}` and may include additional details, as elaborated in the subsequent section.
Evaluators play a crucial role in determining the button's status based on the viewport. For example, users should be restricted from clicking on the mpr if the displaySet is not reconstructable. Additionally, certain buttons within the toolbar may be associated with specific toolGroups and should remain inactive for certain viewports.
Let's look at one of the evaluators (for `evaluate.cornerstoneTool`)
```js
{
name: 'evaluate.cornerstoneTool',
evaluate: ({ viewportId, button }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}
const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return {
disabled: true,
className: '!text-common-bright ohif-disabled',
disabledText: 'Tool not available',
};
}
const isPrimaryActive = toolGroup.getActivePrimaryMouseButtonTool() === toolName;
return {
disabled: false,
className: isPrimaryActive
? '!text-black bg-primary-light'
: '!text-common-bright hover:!bg-primary-dark hover:!text-primary-light',
};
},
},
```
as you can see the job of this evaluator is to determine if the button should be disabled or not. It does so by checking the `toolGroup` and the `toolName` and then returns an object with `disabled` and `className` properties.
The following evaluators are provided by us:
- `evaluate.cornerstoneTool`: If assigned to a button (see next), it will make the button react to the active viewport state based on its toolGroup.
- `evaluate.cornerstoneTool.toggle`: It is designed to consider tools with toggle behavior, such as reference lines and image overlay (either on or off).
- `evaluate.cornerstone.synchronizer`: This is designed to consider the synchronizer state of the viewport, whether it is synced or not.
- `evaluate.viewportProperties.toggle`: Some properties of the viewport are toggleable, such as invert, flip, rotate, etc. By assigning this evaluator to those buttons, they will react to the active viewport state based on its properties. This allows for dynamic buttons that change their appearance based on the active viewport state.
- `evaluate.mpr`: special evaluator for MPR since it needs to check if the displaySet is reconstructable or not.
Sometime you want to use the same `evaluator` for different purposes, in that case you can use an object
with `name` and other properties. For example, in `'evaluate.cornerstone.segmentation'` we use
this pattern, where multiple toolbar buttons are using the same evaluator but with different options (
in this case `toolNames`
)
```js
{
name: 'evaluate.cornerstone.segmentation',
toolNames: ['CircleBrush' , 'SphereBrush']
},
```
#### Viewport and Modality Support Evaluation
The toolbar system now uses a more robust approach for evaluating button states based on viewport types and modalities:
**Viewport Type Support**:
Use `evaluate.viewport.supported` to disable buttons for specific viewport types:
```js
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['volume3d', 'video', 'sm']
}
```
**Modality Support**:
Use `evaluate.modality.supported` to control button state based on modalities:
```js
{
name: 'evaluate.modality.supported',
supportedModalities: ['CT', 'MR'], // Enable only for these modalities
// OR
unsupportedModalities: ['US'] // Disable for these modalities
}
```
#### Composing evaluators
Multiple evaluators can be combined to create complex conditions:
```js
evaluate: [
'evaluate.cine',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['volume3d']
},
{
name: 'evaluate.modality.supported',
supportedModalities: ['CT']
}
]
```
This evaluation system provides more precise control over when toolbar buttons are enabled or disabled based on the active viewport's characteristics.
You can choose to set up multiple evaluators for a single button. This comes in handy when you need to assess the button according to various conditions. For example, we aim to prevent the Cine player from showing up on the 3D viewport, so we have:
```js
evaluate: [
'evaluate.cine',
{
name: 'evaluate.viewport.supported',
unsupportedViewportTypes: ['volume3d'],
},
],
```
You can even come up with advanced evaluators such as:
```js
evaluate: [
'evaluate.cornerstone.segmentation',
// need to put the disabled text last, since each evaluator will
// merge the result text into the final result
{
name: 'evaluate.cornerstoneTool',
disabledText: 'Select the PT Axial to enable this tool',
},
],
```
that we use for our RectangleROIStartEndThreshold tool in tmtv mode.
As you see this evaluator is composed of two evaluators, one is `evaluate.cornerstone.segmentation` which makes sure (in the implementation), that
there is a segmentation created, and the second one is `evaluate.cornerstoneTool` which makes sure that the tool is available in the viewport.
Since we are using multiple evaluators, the `disabledText` of each evaluator will be merged into the final result, so you need to
put the `disabledText` in the last evaluator.
#### Group evaluators
Split buttons (see in [ToolbarService](../../services/data/ToolbarService.md) on how to define one) may feature a group evaluator, we provide two of them and you can write your own.
- `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`: determine the outcome of user interactions with the split buttons on what button should be promoted to the primary section. In the example above, the cornerstone tool's status is checked, and if it is not active in the list of buttons, the button is promoted to the primary section.
- `evaluate.group.promoteToPrimary`: disregarding the cornerstone tool's status and promoting the button to the primary section regardless.
Failure to specify a group evaluator will result in no action, leaving the button in the secondary section.
:::note
As you have learned so far, the extension modules only 'provides' the functionality
and it is the mode's job to consume it. You can next learn how to consume these components
and evaluators to build a toolbar in the
:::
#### Custom Evaluators
You can create your own evaluators. For instance, you have the option to design tri-state buttons, which are buttons with three states such as Show All, Show Some, or Show None of the Viewport Overlays.
## Toolbar buttons consumed in modes
Providing just the components is not enough. You need to add the buttons to the toolbar service and decide which ones are used for each section.
Below we can see a simplified version of the `longitudinal` (basic viewer) 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
function modeFactory({ modeConfiguration }) {
return {
id: 'viewer',
displayName: 'Basic Viewer',
onModeEnter: ({ servicesManager, extensionManager }) => {
const { toolBarService } = servicesManager.services;
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'info',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
]);
},
routes: [
{
path: 'longitudinal',
layoutTemplate: ({ location, servicesManager }) => {
return {
/* */
};
},
},
],
};
}
```
:::note
By default OHIF's default layout (`extensions/default/src/ViewerLayout/index.tsx`) which is used in all modes use a Toolbar component that creates a
`primary` section for tools. That is why we are creating a `primary` section in the example above.
Layouts are also customizable, and you can create your own layout in your extensions and provide it to your modes view `getLayoutTemplateModule` module.
By default we use `@ohif/extension-default.layoutTemplateModule.viewerLayout` to use the default layout which provides a
- Header (with logo on left, toolbar in the middle and user menu on the right)
- Left panel
- Main viewport grid area
- Right panel
:::
## Alternative Toolbar sections
In your UI component, such as panels, you have the option to include a toolbar section template.
This allows you to easily add buttons to it later on. To ensure that the buttons are added properly
to the toolbar, respond to interactions correctly, and evaluate states accurately, simply utilize the `useToolbar` hook.
This hook grants you access to the `onInteraction` function and the `toolbarButtons` array, which you can customize within your UI as needed.
```js
function myCustomPanel({servicesManager}){
const { onInteraction, toolbarButtons } = useToolbar({
servicesManager,
buttonSection: 'myCustomSectionName'
});
// map the buttons to the UI
return (
<div>
{toolbarButtons.map((button, index) => {
return (
<button
key={index}
onClick={() => onInteraction(button)}
>
{button.label}
</button>
);
})}
</div>
);
}
```
We have provided a common component for toolbar buttons called `Toolbox`.
The Toolbox component serves as a versatile and configurable container for toolbar tools within your application.
It is designed to work in conjunction with the useToolbar hook to manage tool states, handle user interactions, and memorize options
using context API.
The `Toolbox` can be easily integrated into your application UI, requiring only the necessary services (servicesManager, commandsManager) and configuration parameters (buttonSectionId, title). Here's a simple usage scenario:
```js
function MyApplication({ servicesManager, commandsManager }) {
// Configuration for the toolbox container
const config = {
servicesManager,
commandsManager,
buttonSectionId: 'customButtonSection',
title: 'My Toolbox',
};
return <Toolbox {...config} />;
}
```
Then in your modes you can edit the tools in that button section.
```js
onModeEnter: ({ servicesManager, extensionManager }) => {
const { toolBarService } = servicesManager.services;
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('customButtonSection', [
'MeasurementTools',
'Zoom',
'info',
]);
},
```
Another example might be you want to open a modal to show some tool options when a button is clicked.
You can use this pattern
```js
// ToolbarButton in mode
{
id: 'Others',
uiType: 'ohif.radioGroup',
props: {
icon: 'info-action',
label: 'Others',
commands: 'showOthersModal',
},
},
```
and inside your mode factory
```js
// adding the 'Others' button to the primary section
toolbarService.createButtonSection('primary', [
'Others', // --------> this one
]);
// adding the shapes button to the 'Other' section
toolbarService.createButtonSection('other', ['Shapes']);
```
here as you see we are using a command `showOthersModal` which is defined in the commands module.
```js
// inside commandsModule of your extension
showOthersModal: () => {
const { uiModalService } = servicesManager.services;
uiModalService.show({
content: OthersModal,
title: 'Others',
customClassName: 'w-8',
movable: true,
contentProps: {
onClose: uiModalService.hide,
servicesManager,
commandsManager,
},
containerDimensions: 'h-[125px] w-[300px]',
contentDimensions: 'h-[125px] w-[300px]',
});
},
```
as you see it is opening a modal with `OthersModal` component (below) which contains the
`Toolbox` component.
```js
// Others modal
import { Toolbox } from '@ohif/ui';
function OthersModal({ servicesManager, commandsManager }) {
return (
<div className="px-2">
<Toolbox
buttonSectionId={'other'}
commandsManager={commandsManager}
servicesManager={servicesManager}
title={'other'}
useCollapsedPanel={false}
></Toolbox>
</div>
);
}
```
The result would be a modal with a toolbox inside it when the `Others` button is clicked, and the
state will get synchronized with the toolbar service automatically.
![alt text](../../../assets/img/toolbox-modal.png)
## Toolbox With Options
Your toolbox toolbar buttons can have options, this is really useful
for advanced tools that require to change some parameters. For example, the brush tool that requires the brush size to change or the mode (2D or 3D).
:::note
Toolbox with options will run the options commands
on the mount of the toolbox component. This is useful for setting the initial state of the toolbox.
:::
Currently we support three types of options.
### Radio option
We use this in segmentation shapes to let the user choose between
three different modes
```js
{
id: 'Shapes',
uiType: 'ohif.radioGroup',
props: {
label: 'Shapes',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
toolNames: ['CircleScissor', 'SphereScissor', 'RectangleScissor'],
},
icon: 'icon-tool-shape',
options: [
{
name: 'Shape',
type: 'radio',
value: 'CircleScissor',
id: 'shape-mode',
values: [
{ value: 'CircleScissor', label: 'Circle' },
{ value: 'SphereScissor', label: 'Sphere' },
{ value: 'RectangleScissor', label: 'Rectangle' },
],
commands: 'setToolActiveToolbar',
},
],
},
},
```
### Range option
We use this for brush radius change
```js
{
id: 'Brush',
icon: 'icon-tool-brush',
label: 'Brush',
evaluate: {
name: 'evaluate.cornerstone.segmentation',
toolNames: ['CircularBrush', 'SphereBrush'],
disabledText: 'Create new segmentation to enable this tool.',
},
options: [
{
name: 'Radius (mm)',
id: 'brush-radius',
type: 'range',
min: 0.5,
max: 99.5,
step: 0.5,
value: 25,
commands: {
commandName: 'setBrushSize',
commandOptions: { toolNames: ['CircularBrush', 'SphereBrush'] },
},
},
],
},
```
### Custom option
We use this pattern inside `tmtv` mode for `RectangleROIThreshold`
```js
{
id: 'RectangleROIStartEndThreshold',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-create-threshold',
label: 'Rectangle ROI Threshold',
commands: setToolActiveToolbar,
evaluate: {
name: 'evaluate.cornerstoneTool',
disabledText: 'Select the PT Axial to enable this tool',
},
options: 'tmtv.RectangleROIThresholdOptions',
},
},
```
Note that it is your job to provide the `tmvt.RectangleROIThresholdOptions` in the getToolbarModule of your extension
## Change Toolbar with hanging protocols
If you want to change the toolbar based on the hanging protocol, you can do a pattern like this.
```js
const { unsubscribe } = hangingProtocolService.subscribe(
hangingProtocolService.EVENTS.PROTOCOL_CHANGED,
() => {
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'WindowLevel',
]);
}
);
```
@@ -0,0 +1,55 @@
---
sidebar_position: 9
sidebar_label: Utility
---
# Module: Utility
## Overview
Often, an extension will need to expose some useful functionality to the other
extensions, or modes that consume the extension. For example, the `cornerstone`
extension, uses its `utility` module to expose methods via
```js
getUtilityModule({ servicesManager }) {
return [
{
name: 'common',
exports: {
getCornerstoneLibraries: () => {
return { cornerstone, cornerstoneTools };
},
getEnabledElement,
CornerstoneViewportService,
dicomLoaderService,
},
},
{
name: 'core',
exports: {
Enums: cs3DEnums,
},
},
{
name: 'tools',
exports: {
toolNames,
Enums: cs3DToolsEnums,
},
},
];
},
};
```
Then a consuming extension can use `getModuleEntry` to access the methods
Below, which is a code from `TrackedCornerstoneViewport` use the `getUtilityModule` method to get the internal `CornerstoneViewportService` which handles the `Cornerstone` viewport.
```js title="extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.tsx"
const utilityModule = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.utilityModule.common'
);
const { CornerstoneViewportService } = utilityModule.exports;
```
@@ -0,0 +1,134 @@
---
sidebar_position: 5
sidebar_label: Viewport
---
# Module: Viewport
## 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 SR (DICOM SR ext.)
- Encapsulated PDFs as PDFs (DICOM pdf ext.)
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.
```jsx
// displaySet, dataSource
const getViewportModule = () => {
const wrappedViewport = props => {
return (
<ExampleViewport
{...props}
onEvent={data => {
commandsManager.runCommand('commandName', data);
}}
/>
);
};
return [{ name: 'example', component: wrappedViewport }];
};
```
## Example Viewport Component
A simplified version of the tracked `OHIFCornerstoneViewport` is shown below, which
creates a cornerstone viewport:
```jsx
function TrackedCornerstoneViewport({
children,
dataSource,
displaySets,
viewportId,
servicesManager,
extensionManager,
commandsManager,
}) {
return (
<div className="viewport-wrapper">
/** Resize Detector */
<ReactResizeDetector
handleWidth
handleHeight
skipOnMount={true} // Todo: make these configurable
refreshMode={'debounce'}
refreshRate={100}
onResize={onResize}
targetRef={elementRef.current}
/>
/** Div For displaying image */
<div
className="cornerstone-viewport-element"
style={{ height: '100%', width: '100%' }}
onContextMenu={e => e.preventDefault()}
onMouseDown={e => e.preventDefault()}
ref={elementRef}
></div>
</div>
);
}
```
### Viewport re-rendering optimizations
We make use of the React memoization pattern to prevent unnecessary re-renders
for the viewport unless certain aspects of the Viewport props change. You can take
a look into the `areEqual` function in the `OHIFCornerstoneViewport` component to
see how this is done.
```js
function areEqual(prevProps, nextProps) {
if (prevProps.displaySets.length !== nextProps.displaySets.length) {
return false;
}
if (
prevProps.viewportOptions.orientation !==
nextProps.viewportOptions.orientation
) {
return false;
}
// rest of the code
```
as you see, we check if the `needsRerendering` prop is true, and if so, we will
re-render the viewport if the `displaySets` prop changes or the orientation
changes.
We use viewportId to identify a viewport and we use it as a key in React
rendering. This is important because it allows us to keep track of the viewport
and its state, and also let React optimize and move the viewport around in the
grid without re-rendering it. However, there are some cases where we need to
force re-render the viewport, for example, when the viewport is hydrated
with a new Segmentation. For these cases, we use the `needsRerendering` prop
to force re-render the viewport. You can add it to the `viewportOptions`
### `@ohif/app`
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
component is used depends on:
- Hanging Protocols
- The Layout Configuration
- Registered SopClassHandlers
![viewportModule-layout](../../../assets/img/viewportModule-layout.png)
<center><i>An example of three cornerstone Viewports</i></center>
@@ -0,0 +1,396 @@
---
sidebar_position: 4
sidebar_label: Internationalization
---
# Viewer: Internationalization
OHIF supports internationalization using [i18next](https://www.i18next.com/)
through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n),
where is the main instance of i18n containing several languages and tools.
<div className="text--center">
<p>Our translation management is powered by
<a href="https://locize.com/" target="_blank" rel="noopener noreferrer"> Locize </a>
through their generous support of open source.</p>
<a href="https://locize.com/" target="_blank" rel="noopener noreferrer" style={{padding: '20px'}}>
<img style={{width:'70px'}} src="https://pbs.twimg.com/profile_images/909709940910120961/oyB0mX2L.jpg" alt="Locize Translation Management Logo"/>
</a>
</div>
## How to change language for the viewer?
You can take a look into user manuals to see how to change the viewer's
language. In summary, you can change the language:
- In the preference modals
- Using the language query in the URL: `lng=Test-LNG`
## Installing
```bash
yarn add @ohif/i18n
# OR
npm install --save @ohif/i18n
```
## How it works
After installing `@ohif/i18n` npm package, the translation function
[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or
[without](#without-react) React.
A translation will occur every time a text match happens in a
[t](https://www.i18next.com/overview/api#t) function.
The [t](https://www.i18next.com/overview/api#t) function is responsible for
getting translations using all the power of i18next.
E.g.
Before:
```html
<div>my translated text</div>
```
After:
```html
<div>{t('my translated text')}</div>
```
If the translation.json file contains a key that matches the HTML content e.g.
`my translated text`, it will be replaced automatically by the
[t](https://www.i18next.com/overview/api#t) function.
---
### With React
This section will introduce you to [react-i18next](https://react.i18next.com/)
basics and show how to implement the [t](https://www.i18next.com/overview/api#t)
function easily.
#### Using Hooks
You can use `useTranslation` hooks that is provided by `react-i18next`
You can read more about this
[here](https://react.i18next.com/latest/usetranslation-hook).
```js
import React from 'react';
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t } = useTranslation();
return <p>{t('my translated text')}</p>;
}
```
### Using outside of OHIF viewer
OHIF Viewer already sets a main
[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to
the shared i18n instance from `@ohif/i18n`, all extensions inside OHIF Viewer
will share this same provider at the end, you don't need to set new providers at
all.
But, if you need to use it completely outside of OHIF viewer, you can set the
I18nextProvider this way:
```jsx
import i18n from '@ohif/i18n';
import { I18nextProvider } from 'react-i18next';
import App from './App';
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>;
```
After setting `I18nextProvider` in your React App, all translations from
`@ohif/i18n` should be available following the basic [With React](#with-react)
usage.
---
### Without React
When needed, you can also use available translations _without React_.
E.g.
```js
import { T } from '@ohif/i18n';
console.log(T('my translated text'));
console.log(T('$t(Common:Play) my translated text'));
```
---
## Main Concepts While Translating
## Namespaces
Namespaces are being used to organize translations in smaller portions, combined
semantically or by use. Each `.json` file inside `@ohif/i18n` npm package
becomes a new namespace automatically.
- Buttons: All buttons translations
- CineDialog: Translations for the tool tips inside the Cine Player Dialog
- Common: all common jargons that can be reused like `t('$t(common:image)')`
- Header: translations related to OHIF's Header Top Bar
- MeasurementTable - Translations for the `@ohif/ui` Measurement Table
- UserPreferencesModal - Translations for the `@ohif/ui` Preferences Modal
- Modals - Translations available for other modals
- PatientInfo - Translations for patients info hover
- SidePanel - Translations for side panels
- ToolTip - Translations for tool tips
### How to use another NameSpace inside the current NameSpace?
i18next provides a parsing feature able to get translations strings from any
NameSpace, like this following example getting data from `Common` NameSpace:
```
$t('Common:Reset')
```
## Extending Languages in @ohif/i18n
Sometimes, even using the same language, some nouns or jargons can change
according to the country, states or even from Hospital to Hospital.
In this cases, you don't need to set an entire language again, you can extend
languages creating a new folder inside a pre existent language folder and
@ohif/i18n will do the hard work.
This new folder must to be called with a double character name, like the `UK` in
the following file tree:
```bash
|-- src
|-- locales
index.js
|-- en
|-- Buttons.json
index.js
| UK
|-- Buttons.js
index.js
| US
|-- Buttons.js
index.js
...
```
All properties inside a Namespace will be merged in the new sub language, e.g
`en-US` and `en-UK` will merge the props with `en`, using i18next's fallback
languages tool.
You will need to export all Json files in your `index.js` file, mounting an
object like this:
```js
{
en: {
NameSpace: {
keyWord1: 'keyWord1Translation',
keyWord2: 'keyWord2Translation',
keyWord3: 'keyWord3Translation',
}
},
'en-UK': {
NameSpace: {
keyWord1: 'keyWord1DifferentTranslation',
}
}
}
```
Please check the `index.js` files inside locales folder for an example of this
exporting structure.
### Extending languages dynamically
You have access to the i18next instance, so you can use the
[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init)
method to add and change language resources as needed.
E.g.
```js
import { i18n } from '@ohif/i18n';
i18next.addResourceBundle('pt-BR', 'Buttons', {
Angle: 'Ângulo',
});
```
---
### How to set a whole new language
To set a brand new language you can do it in two different ways:
- Opening a pull request for `@ohif/i18n` and sharing the translation with the
community. 😍 Please see [Contributing](#contributing-with-new-languages)
section for further information.
- Setting it only in your project or extension:
You'll need a final object like the following, what is setting French as
language, and send it to `addLocales` method.
```js
const newLanguage =
{
fr: {
Commons: {
"Reset": "Réinitialiser",
"Previous": "Précédent",
},
Buttons: {
"Rectangle": "Rectangle",
"Circle": "Cercle",
}
}
```
To make it easier to translate, you can copy the .json files in the /locales
folder and theirs index.js exporters, keeping same keys and NameSpaces.
Importing the main index.js file, will provide you an Object as expected by the
method `addlocales`;
E.g. of `addLocales` usage
```js
import { addLocales } from '@ohif/i18n';
import locales from './locales/index.js';
addLocales(locales);
```
You can also set them manually, one by one, using this
[method](#extending-languages-dynamically).
---
## Test Language
We have created a test language that its translations can be seen in the locales
folder. You can copy paste the folder and its `.json` namespaces and add your
custom language translations.
> If you apply the test-LNG you can see all the elements get appended with 'Test
> {}'. For instance `Study list` becomes `Test Study list`.
## Language Detections
@ohif/i18n uses
[i18next-browser-languageDetector](https://github.com/i18next/i18next-browser-languageDetector)
to manage detections, also exports a method called initI18n that accepts a new
detector config as parameter.
### Changing the language
OHIF Viewer accepts a query param called `lng` in the url to change the
language.
E.g.
```
https://docs.ohif.org/demo/?lng=es-MX
```
### Language Persistence
The user's language preference is kept automatically by the detector and stored
at a cookie called 'i18next', and in a localstorage key called 'i18nextLng'.
These names can be changed with a new
[Detector Config](https://github.com/i18next/i18next-browser-languageDetector).
## Debugging translations
There is an environment variable responsible for debugging the translations,
called `REACT_APP_I18N_DEBUG`.
Run the project as following to get full debug information:
```bash
REACT_APP_I18N_DEBUG=true yarn run dev
```
## Contributing with new languages
We have integrated `i18next` into the OHIF Viewer and hooked it up with Locize
for translation management. Now we need your help to get the app translated into
as many languages as possible, and ensure that we haven't missed pieces of the
app that need translation. Locize has graciously offered to provide us with free
usage of their product.
Once each crowd-sourcing project is completed, we can approve it and merge the
changes into the main project. At that point, the language will be immediately
available on https://viewer.ohif.org/ for testing, and can be used in any OHIF
project. We will support usage through both the Locize CDN and by copying the
language directly into the `@ohif/i18n` package, so that end users can serve the
content from their own domains.
Here are a couple examples:
Spanish:
https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=es
Chinese:
https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=zh
Portuguese:
https://viewer.ohif.org/viewer/1.2.840.113619.2.5.1762583153.215519.978957063.78?lng=pt-BR
Here are some links you can use to sign up to help translate. All you have to do
is sign up, translate the strings, and click Save. On our side, we have a
dashboard to see how many strings are translated and by whom.
This is a pretty random set of languages, so please post below if you'd like a
new language link to be added:
Languages:
[French](https://www.locize.io/register?invitation=Nj8jRPaFKYwtIfNZ6Y5GVOJOpeiXNAdVuSiOg9ceaiveP6uF6y1wVXM9lgfKoYZX)
[German](https://www.locize.io/register?invitation=gChNiVi66YINTPpbKESVAVYPapwg3DkpvMSSomLTvVqBJTXrdmPvxi0WZYHER11q)
[Dutch](https://www.locize.io/register?invitation=2PGe7I184aN0cazM4GXMhzeLtGTf9Zen5uyOEFhHQ8vYkfKHkgR0mJ8dwbNlIeCG)
[Turkish](https://www.locize.io/register?invitation=NOMIXsfneqPbFDqjce5wI7Z6p2swXSjc0rHOH4KLcM6qXSNA4LGyJaLxS7nqWAe3)
[Chinese](https://www.locize.io/register?invitation=lrcUbt7DvV4aJmQeEA4SMAj5xNWr3rltOcaZW1cFc6eod0nvzSPFU4V383tDHGGn)
[Japanese](https://www.locize.io/register?invitation=AaRq2S22o5FsxArwgVuw1gZcQjoe2ffyxarqlAXOpN7JnR2sf2mfamc5qV6LG1Mn)
[Arabic](https://www.locize.io/register?invitation=BiqI6fOm1sC84N3YJLbImXmaOCk8Hc3TMGpXg7NH2R0b0OKuPCp9wlCHLoqMRpfQ)
[Hindi](https://www.locize.io/register?invitation=ph7JmOGTV95DF3EFaI1kvK5Hx98dV9w2wj9h9UhUCWnkBNAwWEdWMcyjnF94zkWb)
[Malay](https://www.locize.io/register?invitation=HsV9F5mKZyeUZYrC3XFRzNI2l0EsIh6hK0MUIKP8IYZA3GxuzfgkvWBLCFwCpDik)
[Russian](https://www.locize.io/register?invitation=da4V9Q8DVO3M1FIlvfT50ZiS8NDNgvC0dE5hHUEAp47FXy6pLXmf1cp2lgLBfLmb)
[Swedish](https://www.locize.io/register?invitation=uR4kzBZC1vhJe6jyMwYXgGPj84QDMulQRlt2s6rONU6ljUh5dgwuUyhJEtZ4REA3)
[Italian](https://www.locize.io/register?invitation=viAS1NC5q342OxtuIv3JFX9DJ3KoR4SmGoElkBlRMphsDKt4hy9bW8JfBjHlfnd7)
[Spanish](https://www.locize.io/register?invitation=ZikXW3KI4w4eo5Cf6L1aQMWaR69XAQ0a9Va3NGorH7mAPvEPXp8w8NLkPNLs5nG8)
[Ukrainian](https://www.locize.io/register?invitation=TY0s6onqH3Asl05Bh1qB44SNSABL2pTYoturwxAmcNKRnzBZFK7bGfn7kVi23Vpg)
[Vietnamese](https://www.locize.io/register?invitation=eqfHDm0vaqxGfQ5TGt6SeV0dx9b2dCp1RrMRdIRavqzOCOAfD3IElzUsyIT689cK)
[Portugese-Brazil](https://www.locize.io/register?invitation=Qc5Dq449xbblQqLTpWeMfsyFiu3gACcgpj0EIucQjjs9Ph9pzPLpq3MnZupF9t6N)
Don't see your language in the above list? Add a request
[here](https://github.com/OHIF/Viewers/issues/618) so that we can create the
language for your translation contribution.
@@ -0,0 +1,4 @@
{
"label": "Managers",
"position": 10
}
@@ -0,0 +1,175 @@
---
sidebar_position: 4
sidebar_label: Commands Manager
---
# 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, in the order specified.
If found, we run the command, passing in any application
or call specific data specified in the command's definition.
The order specified is the REVERSE of the order the modules are registered in
for the mode dependency. That is, the last module registered has the highest priority
and will be searched first for commands. This has nothing to do with when the
command itself is registered, although registrations for the same command in different
modules in the same context will also use last registration wins.
> 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 {
contexts = {};
contextOrder = [];
constructor(_ignoredConfig) {}
getContext(contextName) {
const context = this.contexts[contextName];
return context;
}
/**...**/
createContext(contextName) {
/** ... **/
this.contexts[contextName] = {};
this.contextOrder.push at beginning(contextName)
}
registerCommand(contextName, commandName, definition) {
/**...**/
const context = this.getContext(contextName);
/**...**/
context[commandName] = definition;
}
getCommand(commandName, contextName) {
const useContext = contextName || first context having commandName in contextOrder
return useContext[commandName];
}
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
No methods or configuration is used within the construction.
## 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');
```
@@ -0,0 +1,65 @@
---
sidebar_position: 2
sidebar_label: Extension Manager
---
# Extension Manager
## Overview
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,
});
```
## Events
The following events get published by the `ExtensionManager`:
| Event | Description |
| ---------------------------- | ------------------------------------------------------ |
| ACTIVE_DATA_SOURCE_CHANGED | Fired when the active data source is changed - either replaced with an entirely different one or the existing active data source gets its definition changed via `updateDataSourceConfiguration`. |
## API
The `ExtensionManager` only has the following public API:
- `setActiveDataSource` - Sets the active data source for the application
- `getDataSources` - Returns the registered data sources
- `getActiveDataSource` - Returns the currently active data source
- `getModuleEntry` - Returns the module entry by the give id.
- `addDataSource` - Dynamically adds a data source and optionally sets it as the active data source
- `updateDataSourceConfiguration` - Updates the configuration of a specified data source (name).
- `getDataSourceDef` - Gets the data source definition for a particular data source name.
## Accessing Modules
We use `getModuleEntry` in our `ViewerLayout` logic to find the panels based on
the provided IDs in the mode's configuration.
For instance:
`extensionManager.getModuleEntry("@ohif/extension-measurement-tracking.panelModule.seriesList")`
accesses the `seriesList` panel from `panelModule` of the
`@ohif/extension-measurement-tracking` extension.
```js
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,
};
};
```
@@ -0,0 +1,80 @@
---
sidebar_position: 5
sidebar_label: Hotkeys Manager
---
# Hotkeys Managers
## Overview
`HotkeysManager` handles all the logics for adding, setting and enabling/disabling
the hotkeys.
## Instantiation
`HotkeysManager` is instantiated in the `appInit` similar to the other managers.
```js
const commandsManager = new CommandsManager(commandsManagerConfig);
const servicesManager = new ServicesManager(commandsManager);
const hotkeysManager = new HotkeysManager(commandsManager, servicesManager);
const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
hotkeysManager,
appConfig,
});
```
## Hotkeys Manager API
- `setHotkeys`: The most important method in the `HotkeysManager` which binds the keys with commands.
- `setDefaultHotKeys`: set the defaultHotkeys **property**. Note that, this method **does not** bind the provided hotkeys; however, when `restoreDefaultBindings`
is called, the provided defaultHotkeys will get bound.
- `destroy`: reset the HotkeysManager, and remove the set hotkeys and empty out the `defaultHotkeys`
## Structure of a Hotkey Definition
A hotkey definition should have the following properties:
- `commandName`: name of the registered command
- `commandOptions`: extra arguments to the commands
- `keys`: an array defining the key to get bound to the command
- `label`: label to be shown in the hotkeys preference panel
- `isEditable`: whether the key can be edited by the user in the hotkey panel
### Default hotkeysBindings
The default key bindings can be find in `hotkeyBindings.js`
```js
// platform/core/src/defaults/hotkeyBindings.js
export default [
/**..**/
{
commandName: 'setToolActive',
commandOptions: { toolName: 'Zoom' },
label: 'Zoom',
keys: ['z'],
isEditable: true,
},
{
commandName: 'flipViewportHorizontal',
label: 'Flip Vertically',
keys: ['v'],
isEditable: true,
},
/**..**/
]
```
### Global vs Mode specific hotkeys
You can can set the global hotkeys and override them using the `$set` method
in the customization service.
@@ -0,0 +1,76 @@
---
sidebar_position: 1
sidebar_label: Introduction
---
# Managers
## Overview
`OHIF` uses `Managers` to accomplish various purposes such as registering new
services, dependency injection, and aggregating and exposing `extension`
features.
`OHIF-v3` provides the following managers which we will discuss in depth.
<table>
<thead>
<tr>
<th>Manager</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="./extension">
Extension Manager
</a>
</td>
<td>
Aggregating and exposing modules and features through out the app
</td>
</tr>
<tr>
<td>
<a href="./service">
Services Manager
</a>
</td>
<td>
Single point of registration for all internal and external services
</td>
</tr>
<tr>
<td>
<a href="./commands">
Commands Manager
</a>
</td>
<td>
Register commands with specific context and run commands in the app
</td>
</tr>
<tr>
<td>
<a href="./hotkeys">
Hotkeys Manager
</a>
</td>
<td>
For keyboard keys assignment to commands
</td>
</tr>
</tbody>
</table>
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
<!-- prettier-ignore-end -->
@@ -0,0 +1,204 @@
---
sidebar_position: 3
sidebar_label: Service Manager
---
# Services Manager
## Overview
Services manager is the single point of service registration. Each service needs
to implement a `create` method which gets called inside `ServicesManager` to
instantiate the service. In the app, you can get access to a registered service
via the `services` property of the `ServicesManager`.
## Skeleton
_Simplified_ skeleton of `ServicesManager` is shown below. There are two public
methods:
- `registerService`: registering a new service with/without a configuration
- `registerServices`: registering batch of services
```js
export default class ServicesManager {
constructor(commandsManager) {
this._commandsManager = commandsManager;
this.services = {};
this.registeredServiceNames = [];
}
registerService(service, configuration = {}) {
/** validation checks **/
this.services[service.name] = service.create({
configuration,
commandsManager: this._commandsManager,
});
/* Track service registration */
this.registeredServiceNames.push(service.name);
}
registerServices(services) {
/** ... **/
}
}
```
## Default Registered Services
By default, `OHIF-v3` registers the following services in the `appInit`.
```js title="platform/app/src/appInit.js"
servicesManager.registerServices([
CustomizationService,
UINotificationService,
UIModalService,
UIDialogService,
UIViewportDialogService,
MeasurementService,
DisplaySetService,
ToolBarService,
ViewportGridService,
HangingProtocolService,
CineService,
]);
```
## Service Architecture
If you take a look at the folder of each service implementation above, you will
find out that services need to be exported as an object with `name` and `create`
method.
For instance, `ToolBarService` is exported as:
```js title="platform/core/src/services/ToolBarService/index.js"
import ToolBarService from './ToolBarService';
export default {
name: 'ToolBarService',
create: ({ configuration = {}, commandsManager }) => {
return new ToolBarService(commandsManager);
},
};
```
and the implementation of `ToolBarService` lies in the same folder at
`./ToolbarSerivce.js`.
> Note: The create method is critical for any custom service that you write and
> want to add to the list of services
> Note: For typescript definitions, the service type should be exported
> as part of the Types export on the module. This is recommended going forward
> and existing services will be migrated. As well, the capitalization of the
> name should be lower camel case, with the type being upper camel case. In
> the above example, the service instance should be `toolBarService` with the
> class being `ToolBarService`.
## Accessing Services
Throughout the app you can use `services` property of the service manager to
access the desired service.
For instance in the `PanelMeasurementTableTracking` which is the right panel in
the `longitudinal` mode, we have the _simplified code below_ for downloading the
drawn measurements.
```js
function PanelMeasurementTableTracking({ servicesManager }) {
const { MeasurementService } = servicesManager.services;
/** ... **/
async function exportReport() {
const measurements = MeasurementService.getMeasurements();
/** ... **/
downloadCSVReport(measurements, MeasurementService);
}
/** ... **/
return <> /** ... **/ </>;
}
```
## Registering Custom Services
You might need to write you own custom service in an extension.
`preRegistration` hook inside your extension is the place for registering your
custom service.
```js title="extensions/customExtension/src/index.js"
import WrappedBackEndService from './services/backEndService';
export default {
// ID of the extension
id: 'myExtension',
preRegistration({ servicesManager }) {
servicesManager.registerService(WrappedBackEndService(servicesManager));
},
};
```
and the logic for your service shall be
```js title="extensions/customExtension/src/services/backEndService/index.js"
// Canonical name of upper camel case BackEndService for the class
import BackEndService from './BackEndService';
export default function WrappedBackEndService(servicesManager) {
return {
// Note the canonical name of lower camel case backEndService
name: 'backEndService',
create: ({ configuration = {} }) => {
return new BackEndService(servicesManager);
},
};
}
```
with implementation of
```ts
export default class BackEndService {
constructor(servicesManager) {
this.servicesManager = servicesManager;
}
putAnnotations() {
return post(/*...*/);
}
}
```
with a registration of
```ts title="types/index.ts"
import BackEndService from "../services/BackEndService/BackEndService";
export { BackEndService };
```
# Service Mode Lifecycle
Services may implement initialization and cleanup for mode specific data.
In order to prevent defects where there are differences between initial
and subsequent displays of a study, the contract of the service is that the
state the service is in on mode entry shall be the same whether the mode was
entered or was exited and entered again.
To implement storage/recovery of state, the mode must store the data on
exiting the mode, and restore the data in it's onModeEnter. For example,
the mode may decide to preserve measurement data in the onModeExit, and
to restore it in the onModeEnter. This does not violate the contract since
it is the mode's decision to apply the stored state, and to cache it.
## onModeEnter
A service may implement an onModeEnter call to initialize the service to
be ready for entering a mode.
This is called before the mode `onModeEnter` is called.
## onModeExit
When entering a mode, the service contract states that the service needs to
be in the same state whether it is a fresh load or has previously entered the mode.
The onModeExit allows a service to clean itself up after the mode 'onModeExit'
has stored any persistent data.
@@ -0,0 +1,4 @@
{
"label": "Modes",
"position": 12
}
@@ -0,0 +1,413 @@
---
sidebar_position: 1
sidebar_label: Introduction
---
# Modes
## Overview
A mode can be thought of as a viewer app configured to perform a specific task,
such as tracking measurements over time, 3D segmentation, a guided radiological
workflow, etc. Addition of modes enables _application_ with many _applications_
as each mode become a mini _app configuration_ behind the scene.
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.
<!-- > Modes make extensions (features) more reusable and powerful. -->
OHIF-v3 architecture can be seen in the following:
![mode-archs](../../assets/img/mode-archs.png)
> Note: Templates are now a part of “extensions” Routes are configured by modes
> and/or app
As mentioned, modes are tied to a specific route in the viewer, and multiple
modes/routes can be present within a single application. This allows for
tremendously more flexibility than before you can now:
- Simultaneously host multiple viewers with for different use cases from within
the same app deploy.
- Make radiological viewers for specific purposes/workflows, e.g.:
- Tracking the size of lesions over time.
- PET/CT fusion workflows.
- Guided review workflows optimized for a specific clinical trial.
- Still host one single feature-rich viewer if you desire.
## Anatomy
A mode configuration has a `route` name which is dynamically transformed into a
viewer route on initialization of the application. Modes that are available to a
study will appear in the study list.
![user-study-summary](../../assets/img/user-study-summary.png)
The mode configuration specifies which `extensions` the mode requires, which
`LayoutTemplate` to use, and what props to pass to the template. For the default
template this defines which `side panels` will be available, as well as what
`viewports` and which `displaySets` they may hang.
Mode's config is composed of three elements:
- `id`: the mode `id`
- `modeFactory`: the function that returns the mode specific configuration
- `extensionDependencies`: the list of extensions that the mode requires
that return a config object with certain
properties, the high-level view of this config object is:
```js title="modes/example/src/index.js"
function modeFactory() {
return {
id: '',
version: '',
displayName: '',
onModeEnter: () => {},
onModeExit: () => {},
validationTags: {},
isValidMode: () => {},
routes: [
{
path: '',
init: () => {},
layoutTemplate: () => {},
},
],
extensions: extensionDependencies,
hangingProtocol: [],
sopClassHandlers: [],
hotkeys: []
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
};
export default mode;
```
<table>
<thead>
<tr>
<th align="left">Property</th>
<th align="left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">
id
</td>
<td align="left">unique mode id used to refer to the mode</td>
</tr>
<tr>
<td align="left">
displayName
</td>
<td align="left">actual name of the mode being displayed for each study in the study summary panel</td>
</tr>
<tr>
<td align="left">
<a href="./lifecycle#onModeEnter">
onModeEnter
</a>
</td>
<td align="left">hook is called when the mode is entered by the specified route</td>
</tr>
<tr>
<td align="left">
<a href="./lifecycle#onModeExit">
onModeExit
</a>
</td>
<td align="left">hook is called when the mode exited</td>
</tr>
<tr>
<td align="left">
<a href="">
validationTags
</a>
</td>
<td align="left">validationTags</td>
</tr>
<tr>
<td align="left">
<a href="">
isValidMode
</a>
</td>
<td align="left">Checks if the mode is valid for a study</td>
</tr>
<tr>
<td align="left">
<a href="">
routes
</a>
</td>
<td align="left">route config which defines the route address, and the layout for it</td>
</tr>
<tr>
<td align="left">
<a href="./index#consuming-extensions">
extensionDependencies
</a>
</td>
<td align="left">extensions needed by the mode</td>
</tr>
<tr>
<td align="left">
<a href="">
hanging protocol
</a>
</td>
<td align="left">list of hanging protocols that the mode should have access to</td>
</tr>
<tr>
<td align="left">
<a href="">
sopClassHandlers
</a>
</td>
<td align="left">list of SOPClass modules needed by the mode </td>
</tr>
<tr>
<td align="left">
<a href="">
hotkeys
</a>
</td>
<td align="left">hotkeys</td>
</tr>
</tbody>
</table>
### Consuming Extensions
As mentioned in the [Extensions](../extensions/index.md) section, in `OHIF-v3`
developers write their extensions to create reusable functionalities that later
can be used by `modes`. Now, it is time to describe how the registered
extensions will get utilized for a workflow mode via its `id`.
Each `mode` has a list of its `extensions dependencies` which are the
the `extension` name and version number. In addition, to use a module element you can use the
`${extensionId}.${moduleType}.${element.name}` schema. For instance, if a mode
requires the left panel with name of `AIPanel` that is added by the
`myAIExtension` via the following `getPanelModule` code, it should address it as
`myAIExtension.panelModule.AIPanel` inside the mode configuration file. In the
background `OHIF` will handle grabbing the correct panel via `ExtensionManager`.
```js title="extensions/myAIExtension/getPanelModule.js"
import PanelAI from './PanelAI.js';
function getPanelModule({
commandsManager,
extensionManager,
servicesManager,
}) {
const wrappedAIPanel = () => {
return (
<PanelAI
commandsManager={commandsManager}
servicesManager={servicesManager}
/>
);
};
return [
{
name: 'AIPanel',
iconName: 'list-bullets',
iconLabel: '',
label: 'AI Panel',
isDisabled: studies => {}, // optional
component: wrappedAIPanel,
},
];
}
```
Now, let's look at a simplified code of the `basic viewer` mode which consumes various functionalities
from different extensions.
```js
const extensionDependencies = {
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'@ohif/extension-measurement-tracking': '^3.0.0',
};
const id = 'viewer';
const version = '3.0.0';
function modeFactory({ modeConfiguration }) {
return {
id,
// ...
routes: [
{
// ...
layoutTemplate: ({ location, servicesManager }) => {
return {
id: ohif.layout,
props: {
leftPanels: ['@ohif/extension-measurement-tracking.panelModule.seriesList'],
rightPanels: ['@ohif/extension-measurement-tracking.panelModule.trackedMeasurements'],
viewports: [
{
namespace: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked',
displaySetsToDisplay: ['@ohif/extension-default.sopClassHandlerModule.stack'],
},
],
},
};
},
},
],
extensions: extensionDependencies,
hangingProtocol: ['@ohif/extension-default.hangingProtocolModule.petCT'],
sopClassHandlers: ['@ohif/extension-default.sopClassHandlerModule.stack'],
// ...
};
}
const mode = {
id,
modeFactory,
extensionDependencies,
}
export default mode
```
### Routes
routes config is an array of route settings, and the overall look and behavior
of the viewer at the designated route is defined by the `layoutTemplate` and
`init` functions for the route. We will learn more about each of the above
properties inside the [route documentation](./routes.md)
### HangingProtocols
Currently, you can pass your defined hanging protocols inside the
`hangingProtocols` property of the mode's config. If you specify the hanging protocol
explicitly by its name (only string and not array), it will be THE hanging protocol
that the mode runs with. However, if you specify an array of hanging protocols,
they will get ranked based on the displaySetSelector requirements and the winner
will be the hanging protocol that the mode runs with.
### SopClassHandlers
Mode's configuration also accepts the `sopClassHandler` modules that have been
added by the extensions. This information will get used to initialize `DisplaySetService` with the provided SOPClass modules which
handles creation of the displaySets.
### Hotkeys
`hotkeys` is another property in the configuration of a mode that can be defined
to add the specific hotkeys to the viewer on the mode route. Additionally, the
name under which the hotkeys are stored can be configured as `hotkeyName`.
This allows user customization of the mode specific hotkeys.
```js
// default hotkeys
import { utils } from '@ohif/ui';
const { hotkeys } = utils;
const myHotkeys = [
{
commandName: 'setToolActive',
commandOptions: { toolName: 'Zoom' },
label: 'Zoom',
keys: ['z'],
isEditable: true,
},
{
commandName: 'scaleUpViewport',
label: 'Zoom In',
keys: ['+'],
isEditable: true,
},
]
function modeFactory() {
return {
id: '',
id: '',
displayName: '',
/*
...
*/
hotkeys: {
// The name in preferences to use for this set of hotkeys
// Allows defining different sets for different modes
name: 'custom-hotkey-name',
// And the actual custom values here.
hotkeys:[..hotkeys.defaults.hotkeyBindings, ...myHotkeys]
},
}
}
// exports
```
## Registration
Similar to extension registration, `viewer` will look inside the `pluginConfig.json` to
find the `modes` to register.
```js title=platform/app/pluginConfig.json
// Simplified version of the `pluginConfig.json` file
{
"extensions": [
{
"packageName": "@ohif/extension-cornerstone",
"version": "3.4.0"
},
// ...
],
"modes": [
{
"packageName": "@ohif/mode-longitudinal",
"version": "3.4.0"
}
]
}
```
:::note Important
You SHOULD NOT directly register modes in the `pluginConfig.json` file.
Use the provided `cli` to add/remove/install/uninstall modes. Read more [here](../../development/ohif-cli.md)
:::
The final registration and import of the modes happen inside a non-tracked file `pluginImport.js` (this file is also for internal use only).
:::note
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
For instance we can use
```
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
```
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
:::
@@ -0,0 +1,12 @@
---
sidebar_position: 5
sidebar_label: Installation
---
# Modes: Installation
OHIF-v3 provides the ability to utilize external modes.
You can use ohif `cli` tool to install both local and publicly published
modes on NPM. You can read more [here](../../development/ohif-cli.md)
@@ -0,0 +1,105 @@
---
sidebar_position: 2
sidebar_label: Lifecycle Hooks
---
# Modes: Lifecycle Hooks
## Overview
Currently, there are two hooks that are called for modes:
- onModeInit
- onModeEnter
- onModeExit
## onModeInit
This hook gets run before the defined route has been entered by the mode. This
hook can be used for initialization before the first render.
This is called before `onModeEnter` calls. This allows modes to add or activate their own
data sources and configuration before entering the mode (pre registrations).
## onModeEnter
This hook gets run after the defined route has been entered by the mode. This
hook can be used to initialize the data, services and appearance of the viewer
upon the first render, in any way that is custom to the mode.
This is called after service `onModeEnter` calls so that the entry into a mode
is done in a predefined/fixed state. That allows any restoring of existing state
to be performed.
For instance, in `longitudinal` mode we are using this hook to initialize the
`ToolBarService` and set the window level/width tool to be active and add
buttons to the toolbar.
:::note Tip
In OHIF Version 3.1, there is a new service `ToolGroupService` that is used to
define and manage tools for the group of viewports. This is a new concept
borrowed from the Cornerstone ToolGroup, and you can read more
[here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolgroups/)
:::
```js
function modeFactory() {
return {
id: '',
version: '',
displayName: '',
onModeEnter: ({ servicesManager, extensionManager }) => {
const { ToolBarService, ToolGroupService } = servicesManager.services;
// Init Default and SR ToolGroups
initToolGroups(extensionManager, ToolGroupService);
ToolBarService.addButtons(toolbarButtons);
ToolBarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'MoreTools',
]);
},
/*
...
*/
};
}
```
## onModeExit
This hook is called when the viewer navigates away from the route in the url.
It is called BEFORE the service specific onModeExit calls are performed, and
thus still has access to stateful data which can be cached or stored before
the services clean themselves up.
This is the place for cleaning up NON-service specific data, and services
by unsubscribing to the events. The cleanup of the service itself is intended
to occur in the service `onModeEnter`.
For instance, it can be used to reset the `ToolBarService` which reset the
toggled buttons.
```js
function modeFactory() {
return {
id: '',
displayName: '',
onModeExit: ({ servicesManager, extensionManager }) => {
// Turn of the toggled states on exit
const { ToolBarService } = servicesManager.services;
ToolBarService.reset();
},
/*
...
*/
};
}
```
@@ -0,0 +1,359 @@
---
sidebar_position: 3
sidebar_label: Routes
---
# Mode: Routes
## Overview
Modes are tied to a specific route in the viewer, and multiple modes/routes can
be present within a single application. This makes `routes` config, THE most
important part of the mode configuration.
## Route
`@ohif/app` **compose** extensions to build applications on different routes
for the platform.
Below, you can see a simplified version of the `longitudinal` mode and the
`routes` section which has defined one `route`. Each route has three different
configuration:
- **route path**: defines the route path to access the built application for
that route
- **route init**: hook that runs when application enters the defined route path,
if not defined the default init function will run for the mode.
- **route layout**: defines the layout of the application for the specified
route (panels, viewports)
```js
function modeFactory() {
return {
id: 'viewer',
version: '3.0.0',
displayName: '',
routes: [
{
path: 'longitudinal',
/*init: ({ servicesManager, extensionManager }) => {
//defaultViewerRouteInit
},*/
layoutTemplate: ({ location, servicesManager }) => {
return {
id: ohif.layout,
props: {
leftPanels: [
'@ohif/extension-measurement-tracking.panelModule.seriesList',
],
rightPanels: [
'@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
],
viewports: [
{
namespace:
'@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked',
displaySetsToDisplay: [
'@ohif/extension-default.sopClassHandlerModule.stack',
],
},
{
namespace: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr',
displaySetsToDisplay: [
'@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr',
],
},
],
},
};
},
},
],
/*
...
*/
};
}
```
### Route: path
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.
> Note: Currently, only one route is built for each mode, but we will enhance
> route creation to create separate routes based on the `path` config for each
> `route` object.
There are two types of `routes` that are created by the mode.
- Routes with dataSourceName `/${mode.id}/${dataSourceName}`
- Routes without dataSourceName `/${mode.id}`
Therefore, navigating to
`http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1`
will run the app with the layout and functionalities of the `viewer` mode using
the `defaultDataSourceName` which is defined in the
[App Config](../../configuration/configurationFiles.md)
You can use the same exact mode using a different registered data source (e.g.,
`dicomjson`) by navigating to
`http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1`
### Route: init
The mode also has an init hook, which initializes the mode. If you don't define
an `init` function the `default init` function will get run (logic is located
inside `Mode.jsx`). However, you can define you own init function following
certain steps which we will discuss next.
#### Default init
Default init function will:
- `retriveSeriesMetaData` for the `studyInstanceUIDs` that are defined in the
URL.
- Subscribe to `instanceAdded` event, to make display sets after a series have
finished retrieving its instances' metadata.
- Subscribe to `seriesAdded` event, to run the `HangingProtocolService` on the
retrieves series from the study.
A _simplified_ "pseudocode" for the `defaultRouteInit` is:
```jsx
async function defaultRouteInit({
servicesManager,
studyInstanceUIDs,
dataSource,
}) {
const {
DisplaySetService,
HangingProtocolService,
} = servicesManager.services;
// subscribe to run the function after the event happens
DicomMetadataStore.subscribe(
'instancesAdded',
({ StudyInstanceUID, SeriesInstanceUID }) => {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(seriesMetadata.instances);
}
);
studyInstanceUIDs.forEach(StudyInstanceUID => {
dataSource.retrieve.series.metadata({ StudyInstanceUID });
});
DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {
const studyMetadata = // get study metadata and displaySets
HangingProtocolService.run({studies, displaySets, activeStudy});
});
return unsubscriptions;
}
```
#### Writing a custom init
You can add your custom init function to enhance the default initialization for:
- Fetching annotations from a server for the current study
- Changing the initial image index of the series to be displayed at first
- Caching the next study in the work list
- Adding a custom sort for the series to be displayed on the study browser panel
and lots of other modifications.
You just need to make sure, the mode `dataSource.retrieve.series.metadata`,
`makeDisplaySets` and `run` the HangingProtocols at some point. There are
various `events` that you can subscribe to and add your custom logic. **point to
events**
For instance for jumping to the slice where a measurement is located at the
initial render, you need to follow a pattern similar to the following:
```jsx
init: async ({
servicesManager,
extensionManager,
hotkeysManager,
dataSource,
studyInstanceUIDs,
}) => {
const { DisplaySetService } = servicesManager.services;
/**
...
**/
const onDisplaySetsAdded = ({ displaySetsAdded, options }) => {
const displaySet = displaySetsAdded[0];
const { SeriesInstanceUID } = displaySet;
const toolData = myServer.fetchMeasurements(SeriesInstanceUID);
if (!toolData.length) {
return;
}
toolData.forEach(tool => {
const instance = displaySet.images.find(
image => image.SOPInstanceUID === tool.SOPInstanceUID
);
});
MeasurementService.addMeasurement(/**...**/);
};
// subscription to the DISPLAY_SETS_ADDED
const { unsubscribe } = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
onDisplaySetsAdded
);
/**
...
**/
return unsubscriptions;
};
```
### Route: layoutTemplate
`layoutTemplate` is the last configuration for a certain route in a `mode`.
`layoutTemplate` is a function that returns an object that configures the
overall layout of the application. The returned object has two properties:
- `id`: the id of the `layoutTemplate` being used (it should have been
registered via an extension)
- `props`: the required properties to be passed to the `layoutTemplate`.
For instance `default extension` provides a layoutTemplate that builds the app
using left/right panels and viewports. Therefore, the `props` include
`leftPanels`, `rightPanels` and `viewports` sections. Note that the
`layoutTemplate` defines the properties it is expecting. So, if you write a
`layoutTemplate-2` that accepts a footer section, its logic should be written in
the extension, and any mode that is interested in using `layoutTemplate-2`
**should** provide the `id` for the footer component.
**What module should the footer be registered?**
```js
/*
...
*/
layoutTemplate: ({ location, servicesManager }) => {
return {
id: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
props: {
leftPanels: [
'myExtension.panelModule.leftPanel1',
'myExtension.panelModule.leftPanel2',
],
rightPanels: ['myExtension.panelModule.rightPanel'],
viewports: [
{
namespace: 'myExtension.viewportModule.viewport1',
displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop1'],
},
{
namespace: 'myExtension.viewportModule.viewport2',
displaySetsToDisplay: ['myExtension.sopClassHandlerModule.sop2'],
},
],
},
};
};
/*
...
*/
```
:::note
You can stack multiple panel components on top of each other by providing an array of panel components in the `rightPanels` or `leftPanels` properties.
For instance we can use
```
rightPanels: [[dicomSeg.panel, tracked.measurements], [dicomSeg.panel, tracked.measurements]]
```
This will result in two panels, one with `dicomSeg.panel` and `tracked.measurements` and the other with `dicomSeg.panel` and `tracked.measurements` stacked on top of each other.
:::
## FAQ
> What is the difference between `onModeEnter` and `route.init`
`onModeEnter` gets run first than `route.init`; however, each route can have
their own `init`, but they share the `onModeEnter`.
> How can I change the `workList` appearance or add a new login page?
This is where `OHIF-v3` shines! Since the default `layoutTemplate` is written
for the viewer part, you can simply add a new `layoutTemplate` and use the
component you have written for that route. `Mode` handle showing the correct
component for the specified route.
```js
function modeFactory() {
return {
id: 'viewer',
displayName: '',
routes: [
{
path: 'worklist',
init,
layoutTemplate: ({ location, servicesManager }) => {
return {
id: 'worklistLayout',
props: {
component: 'myNewWorkList',
},
};
},
},
],
/*
...
*/
};
}
```
> How can I navigate to (or show) a different study via the browser history/URL?
There is a command that does this: `navigateHistory`. It takes an object
argument with the `NavigateHistory` type:
```
export type NavigateHistory = {
to: string; // the URL to navigate to
options?: {
replace?: boolean; // replace or add/push to history?
};
};
```
For instance one could bind a hot key to this command to show a specific study
like this...
```
{
commandName: 'navigateHistory',
commandOptions: {
to:
'/viewer?StudyInstanceUIDs=1.2.3',
},
context: 'DEFAULT',
label: 'Nav Study',
keys: ['n'],
isEditable: true,
},
```
@@ -0,0 +1,37 @@
---
sidebar_position: 4
sidebar_label: Validity
---
# Mode: Validity
## Overview
There are two mechanism for checking the validity of a mode for a study.
- `isValidMode`: which is called on a selected study in the workList.
- `validTags`
## isValidMode
This hook can be used to define a function that return a `boolean` which decided the
validity of the mode based on `StudyInstanceUID` and `modalities` that are in the study.
For instance, for pet-ct mode, both `PT` and 'CT' modalities should be available inside the study.
```js
function modeFactory() {
return {
id: '',
displayName: '',
isValidMode: ({ modalities, StudyInstanceUID }) => {
const modalities_list = modalities.split('\\');
const validMode = ['CT', 'PT'].every(modality => modalities_list.includes(modality));
return validMode;
},
/*
...
*/
}
}
```
@@ -0,0 +1,34 @@
---
sidebar_position: 3
---
# PWA vs Packaged
It's important to know that the OHIF Viewer project provides two different build
processes:
```bash
# Static Asset output: For deploying PWAs
yarn run build
```
## Progressive Web Application (PWA)
> [Progressive Web Apps][pwa] are a new breed of web applications that meet the
> [following requirements][pwa-checklist]. Notably, targeting a PWA allows us
> provide a reliable, fast, and engaging experience across different devices and
> network conditions.
The OHIF Viewer is maintained as a [monorepo][monorepo]. We use WebPack to build
the many small static assets that comprise our application. Also generated is an
`index.html` that will serve as an entry point for loading configuration and the
application, as well as a `service-worker` that can intelligently cache files so
that subsequent requests are from the local file system instead of over the
network.
You can read more about this particular strategy in our
[Build for Production Deployment Guide](./../deployment/build-for-production.md)
## Commonjs Bundle (Packaged Script)
We are not supporting `Commonjs` bundling inside `OHIF-v3`.
@@ -0,0 +1,69 @@
---
sidebar_position: 1
---
# Scope of Project
The OHIF Viewer is a web based medical imaging viewer. This allows it to be used
on almost any device, anywhere. The OHIF Viewer is what is commonly referred to
as a ["Dumb Client"][simplicable]
> A dumb client is software that fully depends on a connection to a server or
> cloud service for its functionality. Without a network connection, the
> software offers nothing useful. - [simplicable.com][simplicable]
While the Viewer persists some data, it's scope is limited to caching things
like user preferences and previous query parameters. Because of this, the Viewer
has been built to be highly configurable to work with almost any web accessible
data source.
![scope-of-project diagram](./../assets/img/scope-of-project.png)
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
These can be delivered to your end users however you would like:
- From the local network
- From a remote web server
- From a CDN (content delivery network)
- From a service-worker's cache
- etc.
These "static asset" files are referred to collectively as a "Progressive Web
Application" (PWA), and have the same capabilities and limitations that all PWAs
have.
All studies, series, images, imageframes, metadata, and the images themselves
must come from an external source. There are many, many ways to provide this
information, the OHIF Viewer's scope **DOES NOT** encompass providing _any_
data; only the configuration necessary to interface with one or more of these
many data sources. The OHIF Viewer's scope **DOES** include configuration and
support for services that are protected with OpenID-Connect.
In an effort to aid our users and contributors, we attempt to provide several
[deployment and hosting recipes](../deployment/index.md) as potential starting
points. These are not meant to be rock solid, production ready, solutions; like
most recipes, they should be augmented to best fit you and your organization's
taste, preferences, etc.
## FAQ
_Am I able to cache studies for offline viewing?_
Not currently. A web page's offline cache capabilities are limited and somewhat
volatile (mostly imposed at the browser vendor level). For more robust offline
caching, you may want to consider a server on the local network, or packaging
the OHIF Viewer as a desktop application.
_Does the OHIF Viewer work with the local filesystem?_
It is possible to accomplish this through extensions; however, for a user
experience that accommodates a large number of studies, you would likely need to
package the OHIF Viewer as an [Electron app][electron].
<!--
Links
-->
<!-- prettier-ignore-start -->
[simplicable]: https://simplicable.com/new/dumb-client
[electron]: https://electronjs.org/
<!-- prettier-ignore-end -->
@@ -0,0 +1,4 @@
{
"label": "Services",
"position": 11
}
@@ -0,0 +1,9 @@
---
title: Measurements
---
import { measurementsCustomizations, TableGenerator } from './sampleCustomizations';
{TableGenerator(measurementsCustomizations)}
@@ -0,0 +1,9 @@
---
title: Segmentation
---
import { segmentationCustomizations, TableGenerator } from './sampleCustomizations';
{TableGenerator(segmentationCustomizations)}
@@ -0,0 +1,11 @@
---
title: Study Browser
---
# Study Browser
The Study Browser is a component that allows users to browse and manage studies.
import { studyBrowserCustomizations, TableGenerator } from './sampleCustomizations';
{TableGenerator(studyBrowserCustomizations)}
@@ -0,0 +1,4 @@
{
"label": "Customization Service",
"position": 4
}
@@ -0,0 +1,98 @@
---
title: Advanced Customization
---
Below is an overview of how `transform` and `inheritsFrom` work within this customization system. They allow you to build a hierarchy of customizations in which items can inherit fields from a parent and then optionally apply a transformation before returning the final result.
## `inheritsFrom`
### Purpose
Indicates that the current customization should inherit and merge fields from another customization. The system fetches the parent customization, merges its properties, and returns a combined object.
### How It Works
1. When you request or transform a customization that has `inheritsFrom: "parentCustomizationId"`, the service looks up `parentCustomizationId` via `getCustomization(...)`.
2. Properties from the parent get copied into the child, but the child’s own properties overwrite any matching ones from the parent.
3. If the child has a `transform` function, it runs after the merge.
### Example
```js
export default {
measurementsContextMenu: {
$set: {
inheritsFrom: 'ohif.contextMenu',
menus: [
{
selector: ({ nearbyToolData }) => !!nearbyToolData,
items: [
// ...
],
},
],
},
},
};
```
Here, `measurementsContextMenu` inherits from `ohif.contextMenu`. During retrieval or transformation, the system merges `ohif.contextMenu` into `measurementsContextMenu`.
---
## `transform`
### Purpose
Specifies a function that can modify or enhance the customization object at runtime. Often used to run extra setup code or combine fields in a special way.
### How It Works
1. You define a `transform(customizationService)` function inside your customization object.
2. When the system retrieves the customization, after merging any inherited fields, it calls `transform`.
3. The function may return an updated object, clone existing properties, or apply logic to nested items.
### Example
```js
export default {
'@ohif/contextMenuAnnotationCode': {
$transform: function (customizationService) {
const { code: codeRef } = this;
if (!codeRef) {
throw new Error(`item ${this} has no code ref`);
}
const codingValues = customizationService.getCustomization('codingValues');
const code = codingValues[codeRef];
return {
...this,
codeRef,
code: { ref: codeRef, ...code },
label: this.label || code.text || codeRef,
commands: [{ commandName: 'updateMeasurement' }],
};
},
},
};
```
In this snippet, the `transform` function:
- Reads a code reference from `this`.
- Looks up more data for that code in `codingValues`.
- Merges those details back into `this` before returning the final object.
---
## Common Use Cases
1. **Base and Specialized Customizations**
Use `inheritsFrom` to define a broad, general customization (e.g., a generic context menu) and then create specialized versions that only override certain fields.
2. **Dynamic Assembly**
Use `transform` when you need to compute or modify fields based on application state or other registered customizations.
3. **Nested Items**
If an item within the customization also has `inheritsFrom`, it will follow the same inheritance flow and can run its own `transform` logic.
---
**Key Points**
- `inheritsFrom` is a reference to another customization’s ID.
- If `transform` is defined, it always runs after inheritance is resolved.
- Merging is shallow: child properties override the parent’s.
- You can nest multiple levels of inheritance, each possibly having its own `transform` step.
@@ -0,0 +1,21 @@
---
sidebar_label: Context Menu
sidebar_position: 3
---
# Context Menu
Context menus can be created by defining the menu structure and click
interaction, as defined in the `ContextMenu/types`. There are examples
below specific to the cornerstone context, because the actual click
handler and attributes used to decide when and how to display the menu
are specific to the context used for where the menu is displayed.
## Cornerstone Context Menu
The default cornerstone context menu can be customized by setting the
`cornerstoneContextMenu`. For a full example, see `findingsContextMenu`.
@@ -0,0 +1,83 @@
---
sidebar_label: Custom Routes
sidebar_position: 2
---
# customRoutes
* Name: `routes.customRoutes` global
* Attributes:
** `routes` of type List of route objects (see `route/index.tsx`) is a set of route objects to add.
** Should any element of routes match an existing baked in element, the baked in one will be replaced.
** `notFoundRoute` is the route to display when nothing is found (this has to be at the end of the overall list, so can't be added to routes)
### Example
Since custom routes use React, they should be defined as modules inside the extension that is providing them. And cannot be
in the AppConfig (yet).
```js
export default function getCustomizationModule({ servicesManager, extensionManager }) {
return [
{
name: 'helloPage',
value: {
'routes.customRoutes': {
routes: {
$push: [
{
path: '/custom',
children: () => <h1 style={{ color: 'white' }}>Hello Custom Route</h1>,
},
],
},
},
},
},
]
```
Then after you define the module, you can add it to the customizationService in the AppConfig and reference it by the name you provided.
```js
customizationService: [
// Shows a custom route -access via http://localhost:3000/custom
'@ohif/extension-default.customizationModule.helloPage',
],
```
You can provide multiple custom routes in the same module, for instance another extension can also push to the routes array.
```js
export default function getCustomizationModule({ servicesManager, extensionManager }) {
return [
{
name: 'secondPage',
value: {
customRoutes: {
routes: {
$push: [
{
path: '/second',
children: () => <h1 style={{ color: 'white' }}>Hello Second Route</h1>,
},
],
},
},
},
},
]
}
```
Then you can add it to the customizationService in the AppConfig and reference it by the name you provided.
```js
customizationService: [
// Shows a custom route -access via http://localhost:3000/custom
'@ohif/extension-default.customizationModule.helloPage',
// Shows a custom route -access via http://localhost:3000/second
'@ohif/extension-default.customizationModule.secondPage',
],
```
@@ -0,0 +1,544 @@
---
sidebar_label: Introduction
sidebar_position: 1
---
import { customizations, TableGenerator } from './sampleCustomizations';
import Heading from '@theme/Heading';
import TOCInline from '@theme/TOCInline';
# Customization Service
There are a lot of places where users may want to configure certain elements
differently between different modes or for different deployments. A mode
example might be the use of a custom overlay showing mode related DICOM header
information such as radiation dose or patient age.
The use of `customizationService` enables these to be defined in a typed fashion by
providing an easy way to set default values for this, but to allow a
non-default value to be specified by the configuration or mode.
:::note
`customizationService` itself doesn't implement the actual customization,
but rather just provide mechanism to register reusable prototypes, to configure
those prototypes with actual configurations, and to use the configured objects
(components, data, whatever).
Actual implementation of the customization is totally up to the component that
supports customization.
:::
## General Overview
This framework allows you to configure many features, or "slots," through customization modules. Extensions can choose to offer their own module, which outlines which values can be changed. By looking at each extension's getCustomizationModule(), you can see which objects or components are open to customization.
Below is a high-level example of how you might define a default customization and then consume and override it:
1. **Defining a Customizable Default**
In your extension, you might export a set of default configurations (for instance, a list that appears in a panel). Here, you provide an identifier and store the default list under that identifier. This makes the item discoverable by the customization service:
```js
// Inside your extension’s customization module
export default function getCustomizationModule() {
return [
{
name: 'default',
value: {
defaultList: ['Item A', 'Item B'],
},
},
];
}
```
By naming it `default`, it is automatically registered.
:::info
You might want to have customizations ready to use in your application without actually applying them. In such cases, you can name them something other than `default`. For example, in your mode, you can do this:
```js
customizationService.setCustomizations([
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
]);
```
This is really useful when you want to apply a set of customizations as a pack, kind of like a bundle.
:::
3. **Retrieving the Default Customization**
In the panel or component (or whatever) that needs the list, you retrieve it using `getCustomization`:
```js
const myList = customizationService.getCustomization('defaultList');
// If unmodified, this returns ['Item A', 'Item B']
```
This allows your component to always fetch the most current version (original default or overridden).
4. **Overriding from Outside**
To customize this list outside your extension, call `setCustomizations` with the identifier (`'defaultList'`). For example, a mode can modify the list to add or change items:
```js
// From within a mode (or globally)
customizationService.setCustomizations({
'defaultList': {
$set: ['New Item 1', 'New Item 2'],
},
});
```
The next time any panel calls `getCustomization('defaultList')`, it will get the updated list.
Don't worry we will go over the `$set` syntax in more detail later.
---
## Scope of Customization
Customizations can be declared at three different scopes, each with its own priority and lifecycle. These scopes determine how and when customizations are applied.
### 1. **Default Scope**
- **Purpose**: Establish baseline or "fallback" values that extensions provide.
- **Options**:
1. **Via Extensions**:
- Implement a `getCustomizationModule` function in your extension and name it `default`.
```tsx
function getCustomizationModule() {
return [
{
name: 'default',
value: {
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Default Sort Function',
sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
},
],
},
},
},
];
}
```
2. **Using the `setCustomizations` Method**:
- Call `setCustomizations` in your application and specify `CustomizationScope.Default` as the second argument:
```tsx
customizationService.setCustomizations(
{
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Default Sort Function',
sortFunction: (a, b) => a.SeriesDate - b.SeriesDate,
},
],
},
},
CustomizationScope.Default
);
```
### 2. **Mode Scope**
- **Purpose**: Apply customizations specific to a particular mode.
- **Lifecycle**: These customizations are cleared or reset when switching between modes.
- **Example**: Use the `setCustomizations` method to define mode-specific behavior.
```tsx
customizationService.setCustomizations({
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Mode-Specific Sort Function',
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
},
],
},
});
```
### 3. **Global Scope**
- **Purpose**: Apply system-wide customizations that override both default and mode-scoped values.
- **How to Configure**:
1. Add global customizations directly to the application's configuration file:
```jsx
window.config = {
name: 'config/default.js',
routerBasename: null,
customizationService: [
{
'studyBrowser.sortFunctions': {
$push: [
{
label: 'Global Sort Function',
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
},
],
},
},
],
};
```
2. Use Namespaced Extensions:
- Instead of directly specifying customizations in the configuration, you can refer to a predefined customization module within an extension:
```jsx
window.config = {
name: 'config/default.js',
routerBasename: null,
customizationService: [
'@ohif/extension-cornerstone.customizationModule.newCustomization',
],
};
```
- In this example, the `newCustomization` module within the `@ohif/extension-cornerstone` extension contains the global customizations. The application will load and apply these settings globally.
```tsx
function getCustomizationModule() {
return [
{
name: 'newCustomization',
value: {
'studyBrowser.sortFunctions': {
$push: [
{
label: 'Global Namespace Sort Function',
sortFunction: (a, b) => b.SeriesDate - a.SeriesDate,
},
],
},
},
},
];
}
```
### Priority of Scopes
When a customization is retrieved:
1. **Global Scope**: Takes precedence if defined.
2. **Mode Scope**: Used if no global customization is defined.
3. **Default Scope**: Fallback when neither global nor mode-specific values are available.
As you have guessed the `.setCustomizations` accept a second argument which is the scope. By default it is set to `mode`.
## Customization Syntax
The customization syntax is designed to offer **flexibility** when modifying configurations. Instead of simply replacing values, you can perform granular updates like appending items to arrays, inserting at specific indices, updating deeply nested fields, or applying filters. This flexibility ensures that updates are efficient, targeted, and suitable for complex data structures.
<details>
<summary>
Why a Special Syntax?
</summary>
Traditional value replacement might not be ideal in scenarios such as:
- **Appending or prepending** to an existing list instead of overwriting it.
- **Selective updates** for specific fields in an object without affecting other fields.
- **Filtering or merging** nested items in arrays or objects while preserving other parts.
To address these needs, the customization service uses a **special syntax** inspired by [immutability-helper](https://github.com/kolodny/immutability-helper) commands. Below are examples of each operation.
</details>
---
### 1. Replace a Value (`$set`)
Use `$set` to entirely replace a value. This is the simplest operation which would replace the entire value.
```js
// Before: someKey = 'Old Value'
customizationService.setCustomizations({
someKey: { $set: 'New Value' },
});
// After: someKey = 'New Value'
```
Example with study browser:
```js
// Before: studyBrowser.sortFunctions = []
customizationService.setCustomizations({
'studyBrowser.sortFunctions': {
$set: [
{
label: 'Sort by Patient ID',
sortFunction: (a, b) => a.PatientID.localeCompare(b.PatientID),
},
],
},
});
// After: studyBrowser.sortFunctions = [{label: 'Sort by Patient ID', sortFunction: ...}]
```
---
### 2. Add to an Array (`$push` and `$unshift`)
- **`$push`**: Appends items to the end of an array.
- **`$unshift`**: Adds items to the beginning of an array.
```js
// Before: NumbersList = [1, 2, 3]
// Push items to the end
customizationService.setCustomizations({
'NumbersList': { $push: [5, 6] },
});
// After: NumbersList = [1, 2, 3, 5, 6]
// Unshift items to the front
customizationService.setCustomizations({
'NumbersList': { $unshift: [0] },
});
// After: NumbersList = [0, 1, 2, 3, 5, 6]
```
---
### 3. Insert at Specific Index (`$splice`)
Use `$splice` to insert, replace, or remove items at a specific index in an array.
```js
// Before: NumbersList = [1, 2, 3]
customizationService.setCustomizations({
'NumbersList': {
$splice: [
[2, 0, 99], // Insert 99 at index 2
],
},
});
// After: NumbersList = [1, 2, 99, 3]
```
---
### 4. Merge Object Properties (`$merge`)
Use `$merge` to update specific fields in an object without affecting other fields.
```js
// Before: SeriesInfo = { label: 'Original Label', sortFunction: oldFunc }
customizationService.setCustomizations({
'SeriesInfo': {
$merge: {
label: 'Updated Label',
extraField: true,
},
},
});
// After: SeriesInfo = { label: 'Updated Label', sortFunction: oldFunc, extraField: true }
```
Example with nested merge:
```js
// Before: SeriesInfo = { advanced: { subKey: 'oldValue' } }
customizationService.setCustomizations({
'SeriesInfo': {
advanced: {
$merge: {
subKey: 'updatedSubValue',
},
},
},
});
// After: SeriesInfo = { advanced: { subKey: 'updatedSubValue' } }
```
---
### 5. Apply a Function (`$apply`)
Use `$apply` when you need to compute the new value dynamically.
```js
// Before: SeriesInfo = { label: 'Old Label', data: 123 }
customizationService.setCustomizations({
'SeriesInfo': {
$apply: oldValue => ({
...oldValue,
label: 'Computed Label',
}),
},
});
// After: SeriesInfo = { label: 'Computed Label', data: 123 }
```
---
### 6. Filter and Modify (`$filter`)
Use `$filter` to find specific items in arrays (or objects) and apply changes.
```js
// Before: advanced = {
// functions: [
// { id: 'seriesDate', label: 'Original Label' },
// { id: 'other', label: 'Other Label' }
// ]
// }
customizationService.setCustomizations({
'advanced': {
$filter: {
match: { id: 'seriesDate' },
$merge: {
label: 'Updated via Filter',
},
},
},
});
// After: advanced = {
// functions: [
// { id: 'seriesDate', label: 'Updated via Filter' },
// { id: 'other', label: 'Other Label' }
// ]
// }
```
:::note
Note `$filter` will look recursively for
an object that matches the `match` criteria and then apply the `$merge` or `$set` operation to it.
Note in the example above we are not doing anything with the `functions` array.
:::
Example with deeply nested filter:
```js
// Before: advanced = {
// functions: [{
// id: 'seriesDate',
// viewFunctions: [
// { id: 'axial', label: 'Original Axial' }
// ]
// }]
// }
customizationService.setCustomizations({
'advanced': {
$filter: {
match: { id: 'axial' },
$merge: {
label: 'Axial (via Filter)',
},
},
},
});
// After: advanced = {
// functions: [{
// id: 'seriesDate',
// viewFunctions: [
// { id: 'axial', label: 'Axial (via Filter)' }
// ]
// }]
// }
```
---
### Summary of Commands
| **Command** | **Purpose** | **Example** |
|-------------|-----------------------------------------------|---------------------------------------|
| `$set` | Replace a value entirely | Replace a list or object |
| `$push` | Append items to an array | Add to the end of a list |
| `$unshift` | Prepend items to an array | Add to the start of a list |
| `$splice` | Insert, remove, or replace at specific index | Modify specific indices in a list |
| `$merge` | Update specific fields in an object | Change a subset of fields |
| `$apply` | Compute the new value dynamically | Apply a function to transform values |
| `$filter` | Find and update specific items in arrays | Target nested structures |
| `$transform`| Apply a function to transform the customization | Apply a function to transform values |
## Building Customizations Across Multiple Extensions
Sometimes it is useful to build customizations across multiple extensions. For example, you may want to build a default list of tools inside a vieweport. But then each extension may want to add their own tools to the list.
Lets say i have one default sorting function in my default extension.
```js
function getCustomizationModule() {
return [
{
name: 'default',
value: {
'studyBrowser.sortFunctions': [
{
label: 'Series Number',
sortFunction: (a, b) => {
return a?.SeriesNumber - b?.SeriesNumber;
},
},
],
},
},
];
}
```
This will result in having only series number as the default sorting function.
but now in another extension let's say dicom-seg extension we can add another sorting function.
```js
function getCustomizationModule() {
return [
{
name: "dicom-seg-sorts",
value: {
"studyBrowser.sortFunctions": {
$push: [
{
label: "Series Date",
sortFunction: (a, b) => {
return a?.SeriesDate - b?.SeriesDate;
},
},
],
},
},
},
];
}
```
But since the module is not `default` it will not get applied, but in my segmentation mode i can do
```js
onModeEnter() {
customizationService.setCustomizations([
'@ohif/extension-cornerstone-dicom-seg.customizationModule.dicom-seg-sorts',
]);
}
```
needless to say if you opted to choose `name: default` in the `getCustomizationModule` it was applied globally.
## Customizable Parts of OHIF
Below we are providing the example configuration for global scenario (using the configuration file), however, you can also use the `setCustomizations` method to set the customizations.
{TableGenerator(customizations)}
@@ -0,0 +1,21 @@
---
sidebar_position: 1
---
import { viewportOverlayCustomizations , TableGenerator } from './sampleCustomizations';
# Viewport Overlay
Viewport Overlays are the information that is displayed on the viewport.
![](../../../assets/img/viewportOverlay-customization.png)
There are 4 viewport overlays customization end points
- `viewportOverlay.topRight`
- `viewportOverlay.topLeft`
- `viewportOverlay.bottomLeft`
- `viewportOverlay.bottomRight`
{TableGenerator(viewportOverlayCustomizations)}
@@ -0,0 +1,112 @@
---
sidebar_position: 2
sidebar_label: DICOM Metadata Store
---
# DICOM Metadata Store
## Overview
`DicomMetaDataStore` is the central location that stores the metadata in `OHIF-v3`. There
are several APIs to add study/series/instance metadata and also for getting from the store.
DataSource utilize the `DicomMetaDataStore` to add the retrieved metadata to `OHIF Viewer`.
> In `OHIF-v3` we have significantly changed the architecture of the metadata storage to
> provide a much cleaner way of handling metadata-related tasks and services. Classes such as
> `OHIFInstanceMetadata`, `OHIFSeriesMetadata` and `OHIFStudyMetadata` has been removed and
> replaced with `DicomMetaDataStore`.
>
## Events
There are two events that get publish in `DicomMetaDataStore`:
| Event | Description |
|-----------------|------------------------------------------------------------------------------------------------|
| SERIES_ADDED | Fires when all series of one study have added their summary metadata to the `DicomMetaDataStore` |
| INSTANCES_ADDED | Fires when all instances of one series have added their metadata to the `DicomMetaDataStore` |
## API
Let's find out about the public API for `DicomMetaDataStore` service.
- `EVENTS`: Object including the events mentioned above. You can subscribe to these events
by calling DicomMetaDataStore.subscribe(EVENTS.SERIES_ADDED, myFunction). [Read more about pub/sub pattern here](../pubsub.md)
- `addInstances(instances, madeInClient = false)`: adds the instances' metadata to the store. madeInClient is explained in detail below. After
adding to the store it fires the EVENTS.INSTANCES_ADDED.
- `addSeriesMetadata(seriesSummaryMetadata, madeInClient = false)`: adds series summary metadata. After adding it fires EVENTS.SERIES_ADDED
- `addStudy(study)`: creates and add study-level metadata to the store.
- `getStudy(StudyInstanceUID)`: returns the study metadata from the store. It includes all the series and instance metadata in the requested study
- `getSeries(StudyInstanceUID, SeriesInstanceUID`: returns the series-level metadata for the requested study and series UIDs.
- `getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)`: returns the instance metadata based on the study, series and sop instanceUIDs.
- `geteInstanceFromImageId`: returns the instance metadata based on the requested imageId. It searches the store for the instance that has the same imageId.
### madeInClient
Since upon adding the metadata to the store, the relevant events are fired, and there are
other services that are subscribed to these events (`HangingProtocolService` or `DisplaySetService`), sometimes
we want to add instance metadata but don't want the events to get fired. For instance, when
you are caching the data for the next study in advance, you probably don't want to trigger hanging protocol
logic, so you set `madeInClient=true` to not fire events.
## Storage
As discussed before, there are several API that enables getting metadata from the store and adding to the store.
However, it is good to have an understanding of where they get
stored and in what format and hierarchy. `_model` is a private variable in the store
which holds all the metadata for all studies, series, and instances, and it looks like:
```js title="platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js"
const _model = {
studies: [
{
/** Study Metadata **/
seriesLists: [
{
// Series in study from dicom web server 1 (or different backend 1)
series: [
{
// Series 1 Metadata
instances: [
{
// Instance 1 metadata of Series 1
},
{
// Instance 2 metadata of Series 1
},
/** Other instances metadata **/
],
},
{
// Series 2 Metadata
instances: [
{
// Instance 1 metadata of Series 2
},
{
// Instance 2 metadata of Series 1
},
/** Other instances metadata **/
],
},
],
},
{
// Series in study from dicom web server 2 (or different backend 2)
/** ... **/
},
],
},
],
}
```
@@ -0,0 +1,73 @@
---
sidebar_position: 3
sidebar_label: DisplaySet Service
---
# DisplaySet Service
## Overview
`DisplaySetService` handles converting the `instanceMetadata` into `DisplaySet` that `OHIF` uses for the visualization. `DisplaySetService` gets initialized at service startup time, but is then cleared in the `Mode.jsx`. During the initialization `SOPClassHandlerIds` of the `modes` gets registered with the `DisplaySetService`.
:::tip
DisplaySet is a general set of entities and contains links to bunch of displayable objects (images, etc.) Some series might get split up into different displaySets e.g., MG might have mixed views in a single series, but users might want to have separate LCC, RCC, etc. for hanging protocol usage. A viewport renders a display set into a displayable object.
imageSet is a particular implementation of image displays.
:::
> Based on the instanceMetadata's `SOPClassHandlerId`, the correct module from the registered extensions is found by `OHIF` and its `getDisplaySetsFromSeries` runs to create a DisplaySet for the Series. Note
that this is an ordered operation, and consumes the instances as it proceeds, with the first registered
handlers being able to consume instances first.
DisplaySets are created synchronously when the instances metadata is retrieved and added to the [DicomMetaDataStore](../data//DicomMetadataStore.md). They are ALSO updated when
the DicommetaDataStore receives new data. This update first checks the addInstances
of existing `DisplaySet` values to see if the new instance belongs in an existing set.
Then, the same process is used as was originally done to create new display sets.
NOTE: Any instances not matched are NOT added to any display set and will not be displayed.
## Adding `madeInClient` display sets
It is possible to filter or combine display sets from different series by
performing the filter operation desired, and then calling the `addActiveDisplaySets`
on the new `DisplaySet` instances. This allows operations like combining
two series or sub-selecting a series.
## Events
There are three events that get broadcasted in `DisplaySetService`:
| Event | Description |
| -------------------- | ---------------------------------------------------- |
| DISPLAY_SETS_ADDED | Fires a displayset is added to the displaysets cache |
| DISPLAY_SETS_CHANGED | Fires when a displayset is changed |
| DISPLAY_SETS_REMOVED | Fires when a displayset is removed |
| DISPLAY_SET_SERIES_METADATA_INVALIDATED | Fires when a displayset's series metadata has been altered. An object payload for the event is sent with properties: `displaySetInstanceUID` - the UID of the display set affected; `invalidateData` - boolean indicating if data should be invalidated
## API
Let's find out about the public API for `DisplaySetService`.
- `EVENTS`: Object including the events mentioned above. You can subscribe to these events
by calling DisplaySetService.subscribe(EVENTS.DISPLAY_SETS_CHANGED, myFunction). [Read more about pub/sub pattern here](../pubsub.md)
- `makeDisplaySets(input, { batch, madeInClient, settings } = {}`): Creates displaySet for the provided
array of instances metadata. Each display set gets a random UID assigned.
- `input`: Array of instances Metadata
- `batch = false`: If you need to pass array of arrays of instances metadata to have a batch creation
- `madeInClient = false`: Disables the events firing
- `settings = {}`: Hanging protocol viewport or rendering settings. For instance, setting the initial `voi`, or activating a tool upon
displaySet rendering. [Read more about hanging protocols settings here](./HangingProtocolService.md#Settings)
- `getDisplaySetByUID`: Returns the displaySet based on the DisplaySetUID.
- `getDisplaySetForSOPInstanceUID`: Returns the displaySet that includes an image with the provided SOPInstanceUID
- `getActiveDisplaySets`: Returns the active displaySets
- `deleteDisplaySet`: Deletes the displaySets from the displaySets cache
- `addActiveDisplaySets`: Adds a new display set independently of the make operation.
- `setDisplaySetMetadataInvalidated`: Fires the `DISPLAY_SET_SERIES_METADATA_INVALIDATED` event.
@@ -0,0 +1,367 @@
---
sidebar_position: 4
sidebar_label: Hanging Protocol Service
---
# Hanging Protocol Service
## Overview
This service handles the arrangement of the images in the viewport. In
short, the registered protocols will get matched with the DisplaySets that are
available. Each protocol gets a score, and they are ranked. The
winning protocol (highest score) gets applied and its settings run for the viewports
to be arranged.
You can read more about a HangingProtocol Structure and its properties in the
[HangingProtocol Module](../../extensions/modules/hpModule.md).
The rest of this page is dedicated on how the Hanging Protocol Service works and
what you can do with it.
## Protocols
Protocols are provided by each extension's HangingProtocolModule and are
registered automatically to the HangingProtocolService.
All protocols are stored in the `HangingProtocolService` using their `id` as the key, and the protocol itself as the value.
## Protocol Definition
Protocols are defined in a getHangingProtocolModule inside an extension. As such,
they are defined with a module structure that starts with an id, and has field protocol
that is the actual protocol definition. This setup allows defining more than
one protocol within a module, each one needing it's own definition file.
```javascript
import MyProtocol from './MyProtocol';
export default function getHangingProtocolModule() {
return [
{
id: MyProtocol.id,
protocol: MyProtocol,
},
];
}
```
Within the protocol itself, the structure is laid out as described in the HangingProtocol.ts
type definition, starting with `Protocol`. See the type definition for more details.
## Events
There are two events that get publish in `HangingProtocolService`:
| Event | Description |
| ------------ | -------------------------------------------------------------------- |
| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` |
| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols, or when the applied stage is changed. |
| RESTORE_PROTOCOL | Fires when the protocol or stage is restored, for example, after turning off MPR mode |
| STAGE_ACTIVATION | Fires when the stages are known to have stage.status set. |
## Stage Activation and Status
Sometimes a hanging protocol can be applicable generally, but not all stages
should be shown by default, or should be shown at all. This can be handled by
using the stage activation to control whether the stage is shown by default (`enabled`),
whether it can be navigated to (`passive`) or whether it should not be shown
at all (`disabled`).
The `stage.status` is used to control this, and the status is controlled by
the stage activate. The status values are:
* enabled - meaning that the stage is fully applicable
* passive - meaning that the stage can be applied, but might be missing details
* disabled - meaning that the study has insufficient information for this stage
The default values for no `stageActivation` are to assume that `enabled` has `minViewports` of 1,
and `passive` has `minViewports=0`. That is, enable the stage if at least one
viewport is filled, and make it passive if no viewports are filled.
The setting for these are controlled by the stageActivation property, for example
the following:
```javascript
stageActivation: {
// The enabled activation specifies requirements to enable the stage, that is,
// make it preferred.
enabled: {
// The default value here is 1, and indicates how many non-blank viewports
// are required.
minViewportsMatched: 3,
// This enables specifying cross cutting concerns, such as having a stage
// only apply to males or females, and is a list of display set selector ids
displaySetSelectorsMatched: ['dsMale'],
},
// The passive check is performed first. If it fails, the enabled is NOT
// checked, but the status set to disabled. The default passive check
// should always be passed, so it is fine to just define enabled if desired.
passive: {
// The default is 0, which means allow the stage even if no viewports are
// filled. This allows dragging and dropping into the viewports to
// make matches manually, which can then be re-used for other stages.
minViewportsMatched: 0,
displaySetSelectorsMatched: [...],
},
}
```
## API
- `destroy`: Destroys the HP service
- `reset` and `onModeEnter`: Resets the HP service to not have any active
hanging protocols
- `getActiveProtocol`: Returns an object of the internal state of the HP service,
useful for storing said state, as well as for getting direct access to the
protocol and stage objects. Users of this should count on it being not completely
stable as to exactly what this returns, as internal details can change.
- `getState`: Returns the currently applied protocol ID, stage index and active study UID.
This information is storable/usable as state information to be used elsewhere.
- `getDefaultProtocol`: Returns the default protocol to apply.
- `getMatchDetails`: returns an object which contains the details of the
matching for the viewports, displaySets and whether the protocol is
applied to the viewport or not yet. This is deprecated as it is expected
to be communicated by events instead.
- `getProtocols`: Returns a list of the currently active protocols.
- `getProtocolById`: Gets the protocol with the given id.
- `addProtocol`: adds provided protocol to the list of registered protocols
for matching. Will replacing any protocol with the same id, allowing, for example,
to replace the default protocol.
- `setActiveProtocols`: Choose the protocols which are active. Can take a
single protocol id or a list. When a single one is provided, that one will be
applied whether or not the required rules match. Called automatically on mode
init.
- `setActiveStudyUID`: Sets the given study UID as active, which has significance
in terms of the matching rules being able to match against the active study.
- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided
studyMetaData and optional protocolId. If protocol is not given, HP Matching
engine will search all the registered protocols for the best matching one
based on the constraints.
- `registerImageLoadStrategy`: Adds a custom image load strategy.
- `addCustomAttribute`: adding a custom attribute for matching. (see below)
- `setProtocol`: applies a protocol to the current studies, it can be used for instance to apply a
hanging protocol when pressing a button in the toolbar. We use this for applying 'mpr'
hanging protocol when pressing the MPR button in the toolbar. `setProtocol` will
accept a set of options that can be used to define the displaySets that will be
used for the protocol. If no options are provided, all displaySets will
be used to match the protocol.
- `getStageIndex`: Finds the stage index for a given set of match keys. Currently
only works on the currently active protocol, but is supposed to be able to work
with other protocols as well.
- `getMissingViewport`: Returns a viewport object to be used as the missing
viewport instance. This is used to fill out new viewports.
Default initialization of the modes handles running the `HangingProtocolService`
## Hanging Protocol Instance Definition
A hanging protocol has an id provided in the module which is used to identify
the protocol. Mostly these should include the module name so that they
do not overlap, with the suggested id being `${moduleId}.${simpleName}`. The
'default' name is used as the hanging protocol id when no other protocol applies,
and can be set as the last module listed containing 'default'.
A hanging protocol can also be defined with a generator.
A generator is a function we can write this way:
```ts
function protocolGenerator({ servicesManager, commandsManager }) {
// Some computations using services and commands ...
return {
protocol: generatedProtocol
}
}
```
See the typescript definitions for more details on the structure of protocols.
## Additional viewports for layout - `defaultViewport`
Sometimes the user manually selects a layout of a given size, say `2x3`. The
hanging protocol can define what viewport options to use for this viewport by
defining an extra viewport option in `defaultViewport`. For example:
```javascript
defaultViewport: {
viewportOptions: {
viewportType: 'stack',
toolGroupId: 'default',
allowUnmatchedView: true,
},
displaySets: [
{
id: 'defaultDisplaySetId',
matchedDisplaySetsIndex: -1,
},
],
},
```
This allows defining the type of additional viewports, what tool group etc they
are allowed in, and which display set is used to fill them. In the above case,
the display set is the same as the other viewports, but the
`matchedDisplaySetsIndex=-1`, so that means find the next matching display set
from the display set selector which isn't already filling a view.
## Custom Attribute
In some situations, you might want to match based on a custom
attribute and not the DICOM tags. For instance,
if you have assigned a `timepointId` to each study, and you want to match based on it.
Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching.
There are various ways that you can let `HangingProtocolService` know of you
custom attribute. We will show how to add it inside the mode configuration.
```js
const defaultProtocol = {
id: 'defaultProtocol',
/** ... **/
protocolMatchingRules: [
{
weight: 3,
attribute: 'timepoint',
constraint: {
equals: 'first',
},
required: false,
},
],
displaySetSelectors: {
/** ... */
}
stages: [
/** ... **/
],
numberOfPriorsReferenced: -1,
};
// Custom function for custom attribute
const getTimePointUID = metaData => {
// requesting the timePoint Id
return myBackEndAPI(metaData);
};
function modeFactory() {
return {
id: 'myMode',
/** .. **/
routes: [
{
path: 'myModeRoute',
init: async ({}) => {
const {
DicomMetadataStore,
HangingProtocolService,
} = servicesManager.services;
const onSeriesAdded = ({
StudyInstanceUID,
madeInClient = false,
}) => {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
// Adding custom attribute to the hangingprotocol
HangingProtocolService.addCustomAttribute(
'timepoint',
'timepoint',
metaData => getFirstMeasurementSeriesInstanceUID(metaData)
);
HangingProtocolService.run(studyMetadata);
};
DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
onSeriesAdded
);
},
},
],
/** ... **/
};
}
```
### Custom Attributes for Viewport Options
The custom attributes can also be used for viewport options. This example,
from the default hanging protocol navigates the image to the image
specified in the URL:
```javascript
viewportOptions: {
initialImageOptions: {
// custom attribute name is selected by 'custom'
custom: 'sopInstanceLocation',
// This is the value returned if the above doesn't return anything
defaultValue: { index: 5 },
}
}
```
### Included Custom Attributes
A few custom attributes are included under @ohif/extension-test, these are namely:
*sameAs
*maxNumImageFrames
*numberOfDisplaySets
To use these included custom attributes, the extension will need to be enabled under platform/app/pluginConfig.json:
```javascript
{
"extensions": [
...
{
"packageName": "@ohif/extension-test",
"version": "3.4.0"
},
...
]
}
```
Furthermore, the extension will also need to be included under extensionDependencies in the desired mode (e.g. modes/tmtv/src/index.js):
```javascript
const extensionDependencies = {
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'@ohif/extension-tmtv': '^3.0.0',
'@ohif/extension-test': '^0.0.1',
};
```
The below example modifies the included hanging protocol (extensions/tmtv/src/getHangingProtocolModule.js) and uses the sameAs attribute included in the @ohif/extension-test extension to check that the selected PT has the same frame of reference as the CT:
```javascript
ptDisplaySet: {
...
seriesMatchingRules: [
{
attribute: 'sameAs',
sameAttribute: 'FrameOfReferenceUID',
sameDisplaySetId: 'ctDisplaySet',
constraint: {
equals: {
value: true,
},
},
required: true,
},
...
```
@@ -0,0 +1,182 @@
---
sidebar_position: 6
sidebar_label: Measurement Service
---
# Measurement Service
## Overview
`MeasurementService` handles the internal measurement representation inside
`OHIF` platform. Developers can add their custom `sources` with `mappers` to
enable adding measurements inside OHIF. Currently, we are maintaining
`CornerstoneTools` annotations and corresponding mappers can be found inside the
`cornerstone` extension. However, `MeasurementService` can be configured to work
with any custom tools given that its `mappers` is added to the
`MeasurementService`. We can see the overall architecture of the
`MeasurementService` below:
![services-measurements](../../../assets/img/services-measurements.png)
## Events
There are seven events that get publish in `MeasurementService`:
| Event | Description |
| --------------------- | ------------------------------------------------------ |
| MEASUREMENT_UPDATED | Fires when a measurement is updated |
| MEASUREMENT_ADDED | Fires when a new measurement is added |
| RAW_MEASUREMENT_ADDED | Fires when a raw measurement is added (e.g., dicom-sr) |
| MEASUREMENT_REMOVED | Fires when a measurement is removed |
| MEASUREMENTS_CLEARED | Fires when all measurements are deleted |
| JUMP_TO_MEASUREMENT_VIEWPORT | Fires when a measurement is requested to be jumped to, applying to individual viewports. |
| JUMP_TO_MEASUREMENT_LAYOUT | Fires when a measurement is requested to be jumped to, applying to the overall layout. |
## API
- `getMeasurements`: returns array of measurements
- `getMeasurement(id)`: returns the corresponding measurement based on the
provided Id.
- `remove(id, source)`: removes a measurement and broadcasts the
`MEASUREMENT_REMOVED` event.
- `clearMeasurements`: removes all measurements and broadcasts
`MEASUREMENTS_CLEARED` event.
- `createSource(name, version)`: creates a new measurement source, generates a
uid and adds it to the `sources` property of the service.
- `addMapping(source, definition, matchingCriteria, toSourceSchema, toMeasurementSchema)`:
adds a new measurement matching criteria along with mapping functions. We will
learn more about [source/mappers below](#source--mappers)
- `update`: updates the measurement details and fires `MEASUREMENT_UPDATED`
- `addRawMeasurement(source,definition,data,toMeasurementSchema,dataSource = {}`
: adds a raw measurement into a source so that it may be converted to/from
annotation in the same way. E.g. import serialized data of the same form as
the measurement source. Fires `MEASUREMENT_UPDATED` or `MEASUREMENT_ADDED`.
Note that, `MeasurementService` handles finding the correct mapper upon new
measurements; however, `addRawMeasurement` provides more flexibility. You can
take a look into its usage in `dicom-sr` extension.
- `source`: The measurement source instance.
- `definition`: The source definition you want to add the measurement to.
- `data`: The data you wish to add to the source.
- `toMeasurementSchema`: A function to get the `data` into the same shape as
the source definition.
- `jumpToMeasurement(viewportId, id)`: calls the listeners who have
subscribed to `JUMP_TO_MEASUREMENT`.
## Source / Mappers
To create a custom measurement source and relevant mappers for each tool, you
can take a look at the `init.js` inside the `cornerstone` extension. In which we
are registering our `CornerstoneTools-v4` measurement source to
MeasurementService. Let's take a peek at the _simplified_ implementation
together. To achieve this, for each tool, we need to provide three mappers:
- `matchingCriteria`: criteria used for finding the correct mapper for the drawn
tool.
- `toAnnotation`: tbd
- `toMeasurement`: a function that converts the tool data to OHIF internal
representation of measurement data.
```js title="extensions/cornerstone/src/utils/measurementServiceMappings/Length.js"
function toMeasurement(
csToolsAnnotation,
DisplaySetService,
getValueTypeFromToolType
) {
const { element, measurementData } = csToolsAnnotation;
/** ... **/
const {
SOPInstanceUID,
FrameOfReferenceUID,
SeriesInstanceUID,
StudyInstanceUID,
} = getSOPInstanceAttributes(element);
const displaySet = DisplaySetService.getDisplaySetForSOPInstanceUID(
SOPInstanceUID,
SeriesInstanceUID
);
/** ... **/
return {
id: measurementData.id,
SOPInstanceUID,
FrameOfReferenceUID,
referenceSeriesUID: SeriesInstanceUID,
referenceStudyUID: StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
label: measurementData.label,
description: measurementData.description,
unit: measurementData.unit,
length: measurementData.length,
type: getValueTypeFromToolType(tool),
points: getPointsFromHandles(measurementData.handles),
};
}
//////////////////////////////////////////
// extensions/cornerstone/src/init.js
const Length = {
toAnnotation,
toMeasurement,
matchingCriteria: [
{
valueType: MeasurementService.VALUE_TYPES.POLYLINE,
points: 2,
},
],
};
const _initMeasurementService = (MeasurementService, DisplaySetService) => {
/** ... **/
const csToolsVer4MeasurementSource = MeasurementService.createSource(
'CornerstoneTools',
'4'
);
/* Mappings */
MeasurementService.addMapping(
csToolsVer4MeasurementSource,
'Length',
Length.matchingCriteria,
toAnnotation,
toMeasurement
);
/** Other tools **/
return csToolsVer4MeasurementSource;
};
```
## Auto complete
Use a customization service to add more customizations for measurement labels. Later, when adding a measurement, the user will be prompted to choose from a list of labels.
```js
customizationService.addModeCustomizations([
{
id: 'measurementLabels',
labelOnMeasure: true,
exclusive: true,
items: [
{ value: 'Head', label: 'Head' },
{ value: 'Neck', label: 'Neck' },
{ value: 'Knee', label: 'Knee' },
{ value: 'Toe', label: 'Toe' },
],
},
]);
```
@@ -0,0 +1,71 @@
---
sidebar_position: 5
sidebar_label: Multi Monitor Service
---
# Multi Monitor Service
::: info
We plan to enhance this service in the future. Currently, it offers a basic implementation of multi-monitor support, allowing you to manually open multiple windows on the same monitor. It is not yet a full multi-monitor solution!
:::
The multi-monitor service provides detection, launch and communication support
for multiple monitors or windows/screens within a single monitor.
:::info
The multi-monitor service is currently applied via configuration file.
```js
customizationService: ['@ohif/extension-default.customizationModule.multimonitor'],
```
:::
## Configurations
The service supports two predefined configurations:
1. **Split Screen (`multimonitor=split`)**
Splits the primary monitor into two windows.
2. **Multi-Monitor (`multimonitor=2`)**
Opens windows across separate physical monitors.
### Launch Methods
- Specify `&screenNumber=0` to designate the first window explicitly.
- Omit `screenNumber` to let the service handle window assignments dynamically.
- Use `launchAll` in the query parameters to launch all configured screens simultaneously.
#### Example URLs:
- **Split Screen:**
`http://viewer.ohif.org/.....&multimonitor=split`
Splits the primary monitor into two windows when a study is viewed.
- **Multi-Monitor with All Screens:**
`http://viewer.ohif.org/.....&multimonitor=2&screenNumber=0&launchAll`
Launches two monitors and opens all configured screens.
---
## Behavior
### Refresh, Close and Open
If you refresh the base/original window, then all the other windows will also
refresh. However, you can safely refresh any single other window, and on the next
command to the other windows, it will re-create the other window links without
losing content in the other windows. You can also close any other window and
it will be reopened the next time you try to call to it.
## Executing Commands
The MultiMonitorService adds the ability to run commands on other specified windows.
This allows opening up a study on another window without needing to refresh
it's contents. The command below shows an example of how this can be done:
@@ -0,0 +1,49 @@
---
sidebar_position: 8
sidebar_label: Panel Service
---
# Panel Service
## Overview
The Panel Service provides for activating/showing a panel that was registered
via the `getPanelModule` extension method. Such panels can be either explicitly
activated or implicitly triggered to activate when some other event occurs.
## Events
The following events are published in `PanelService`.
| Event | Description |
| --------------------- | ------------------------------------------------------ |
| ACTIVATE__PANEL | Fires a `ActivatePanelEvent` when a particular panel should be activated (i.e. shown). |
## API
### Panel Activation
- `activatePanel`: Fires the `ACTIVATE_PANEL` event for a particular panel (id).
An optional `forceActive` flag can be passed that when `true` "forces" a
panel to show. Ultimately, it is up to a panel's container whether it
is appropriate to activate/show the panel. For instance, if the user opened and then
closed a side panel that contains the panel to activate, that side panel
may decide that the user knows best and will not open the panel (again).
- `addActivatePanelTriggers`: Creates and returns event subscriptions that when
fired will activate the specified panel with an optional `forceActive` flag
(see `activatePanel`). This allows for panel activation to be directly triggered
by some other event(s). When the triggers are no longer needed, simply
unsubscribe to the returned subscriptions. For example, a panel
for tracking measurements might get activated every time the
`MeasurementService` fires a `MEASUREMENT_ADDED` event like this:
```js
panelService.addActivatePanelTriggers('measurement-tracking-panel-id', [
sourcePubSubService: measurementService,
sourceEvents: [
measurementService.EVENTS.MEASUREMENT_ADDED,
measurementService.EVENTS.RAW_MEASUREMENT_ADDED,
],
]);
```
@@ -0,0 +1,145 @@
---
sidebar_position: 7
sidebar_label: Segmentation Service
---
# Segmentation Service
## Events
```typescript
SEGMENTATION_MODIFIED // When a segmentation is updated
SEGMENTATION_DATA_MODIFIED // When segmentation data changes
SEGMENTATION_ADDED // When new segmentation is added
SEGMENTATION_REMOVED // When segmentation is removed
SEGMENT_LOADING_COMPLETE // When segment group adds pixel data to volume
SEGMENTATION_LOADING_COMPLETE // When full segmentation volume is filled
```
## Core APIs
### Creation Methods
```typescript
createLabelmapForDisplaySet(
displaySet,
{
segmentationId?: string,
label: string,
segments?: {
[segmentIndex: number]: Partial<Segment>
}
}
)
```
### Segmentation Management
```typescript
setActiveSegmentation(viewportId, segmentationId)
getSegmentations()
getSegmentation(segmentationId)
jumpToSegmentCenter(segmentationId, segmentIndex, viewportId)
highlightSegment(segmentationId, segmentIndex, viewportId)
```
### Segment Operations
```typescript
addSegment(segmentationId, {
segmentIndex?: number,
label?: string,
color?: [number, number, number, number], // RGBA
visibility?: boolean,
isLocked?: boolean,
active?: boolean
})
setSegmentColor(viewportId, segmentationId, segmentIndex, color)
setSegmentVisibility(viewportId, segmentationId, segmentIndex, visibility)
```
## Data Structures
### Segmentation Object
```typescript
interface Segmentation {
segmentationId: string;
label: string;
segments: {
[segmentIndex: number]: {
segmentIndex: number;
label: string;
locked: boolean;
cachedStats: { [key: string]: unknown };
active: boolean;
}
};
representationData: RepresentationsData;
}
```
## Code Examples
### Creating a Segmentation
```typescript
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
const segmentationId = await segmentationService.createLabelmapForDisplaySet(
displaySet,
{
label: 'New Segmentation',
segments: {
1: {
label: 'First Segment',
active: true
}
}
}
);
```
### Managing Active Segmentations
```typescript
segmentationService.setActiveSegmentation('viewport-1', segmentationId);
```
### Adding Segments
```typescript
segmentationService.addSegment(segmentationId, {
label: 'Tumor',
color: [255, 0, 0, 255], // RGBA format
active: true
});
```
### Visibility Management
```typescript
// Set segment visibility
segmentationService.setSegmentVisibility(
'viewport-1',
segmentationId,
1, // segmentIndex
true // visible
);
// Get viewport IDs with segmentation
const viewportIds = segmentationService.getViewportIdsWithSegmentation(segmentationId);
```
### Segment Styling
```typescript
// Set segment color
segmentationService.setSegmentColor(
'viewport-1',
segmentationId,
1, // segmentIndex
[255, 0, 0, 255] // RGBA
);
```
@@ -0,0 +1,147 @@
---
sidebar_position: 8
sidebar_label: SyncGroup Service
---
# Sync Group Service
## Overview
The `SyncGroupService` is responsible for managing synchronization groups in the OHIF Viewer. Synchronization groups allow multiple viewports to be synchronized based on various criteria, such as camera position, window level, zoom/pan, and image slice position. This service provides a centralized way to create, update, and manage synchronization groups.
Right now, synchronization groups can be defined in the hanging protocols or manually assigning buttons.
## API
- `getSyncCreatorForType(type)`: Returns the synchronizer creator function for the specified type.
- `addSynchronizerType(type, creator)`: Adds a new synchronizer type with a custom creator function.
- `getSynchronizer(id)`: Retrieves a synchronizer by its ID.
- `getSynchronizersOfType(type)`: Retrieves an array of synchronizers of the specified type.
- `addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups)`: Adds a viewport to one or more synchronization groups.
- `destroy()`: Destroys all synchronizers.
- `getSynchronizersForViewport(viewportId)`: Retrieves an array of synchronizers associated with the specified viewport.
- `removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroupId?)`: Removes a viewport from a specific synchronization group or all synchronization groups if no group ID is provided.
## Usage
### Via hanging protocols
You can set up different types of synchronization groups for your viewports. For example, in the TMTV hanging protocol (`extensions/tmtv/src/getHangingProtocolModule.js`), we can see how different synchronization groups are defined for various viewports:
```javascript
const ptAXIAL = {
viewportOptions: {
// ...
syncGroups: [
{
type: 'cameraPosition',
id: 'axialSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptWLSync',
source: true,
target: true,
},
{
type: 'voi',
id: 'ptFusionWLSync',
source: true,
target: false,
options: {
syncInvertState: false,
},
},
],
},
// ...
};
```
In this example, the `ptAXIAL` viewport is part of three synchronization groups:
1. `cameraPosition` group with the ID `'axialSync'`: This group synchronizes the camera position across viewports that are both source and target.
2. `voi` (Window Level) group with the ID `'ptWLSync'`: This group synchronizes the window level settings across viewports that are both source and target.
3. `voi` group with the ID `'ptFusionWLSync'`: This group synchronizes the window level settings, but the `ptAXIAL` viewport is only a source, not a target.
:::tip
You can control the state of the synchronizer via a toolbar button after you define the synchronization group in the hanging protocol.
```js
{
id: 'SyncToggle',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-info',
label: 'toggle',
commands: {
commandName: 'toggleSynchronizer',
commandOptions: {
syncId: 'axialSync'
}
}
},
},
```
as you can see by using the `toggleSynchronizer` command you can toggle the state of the synchronizer for the specified syncId.
:::
### Manually through a button
You can create a button on the toolbar that you provice the synchronization group type,
and it applys it to all viewports.
:::note
Currently we don't have a proper way to select viewports to apply the synchronization group to. It is applied to all applicable viewports
:::
For instance look at `imageSliceSync` button in the longitudinal mode (`modes/longitudinal/src/moreTools.ts`) and how it runs a command
```js
ToolbarService.createButton({
id: 'ImageSliceSync',
icon: 'link',
label: 'Image Slice Sync',
tooltip: 'Enable position synchronization on stack viewports',
commands: [
{
commandName: 'toggleSynchronizer',
commandOptions: {
type: 'imageSlice',
},
},
],
})
```
You can create another button to toggle 'voi' synchronization. Currently we group
viewports by modality and apply the voi synchronization to all viewports of the same modality.
```js
ToolbarService.createButton({
id: 'VoiSync',
icon: 'link',
label: 'VOI Sync',
tooltip: 'Enable VOI synchronization on viewports',
commands: [
{
commandName: 'toggleSynchronizer',
commandOptions: {
type: 'voi',
},
},
],
})
```
:::tip
For your custom synchronization groups, you can create a new synchronizer type and follow the
same pattern as the existing synchronizers.
:::
@@ -0,0 +1,89 @@
---
sidebar_position: 7
sidebar_label: ToolGroup Service
---
# Tool Group Service
## Overview
The `ToolGroupService` is responsible for managing tool groups in the OHIF Viewer.
:::tip
Read more about toolGroups [here](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/toolGroups)
:::
It allows you to create, update, and manage tool groups and the tools associated with them. Tool groups are used to organize and control the behavior of various tools in the viewer, such as window level, pan, zoom, measurements, and annotations.
## Events
The `ToolGroupService` emits the following events:
| Event | Description |
| ---------------------------------- | ----------------------------------------------- |
| `VIEWPORT_ADDED` | Fires when a viewport is added to a tool group |
| `TOOLGROUP_CREATED` | Fires when a new tool group is created |
## API
- `getToolGroup(toolGroupId?)`: Retrieves a tool group by its ID. If no ID is provided, it returns the tool group for the active viewport.
- `getToolGroupIds()`: Returns an array of all tool group IDs.
- `getToolGroupForViewport(viewportId)`: Returns the tool group associated with the specified viewport.
- `getActiveToolForViewport(viewportId)`: Returns the active tool for the specified viewport.
- `destroy()`: Destroys all tool groups.
- `destroyToolGroup(toolGroupId)`: Destroys the specified tool group.
- `removeViewportFromToolGroup(viewportId, renderingEngineId, deleteToolGroupIfEmpty?)`: Removes a viewport from a tool group. If `deleteToolGroupIfEmpty` is true and the tool group becomes empty after removing the viewport, it will be destroyed.
- `addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId?)`: Adds a viewport to a tool group. If `toolGroupId` is not provided, the viewport will be added to all tool groups.
- `createToolGroup(toolGroupId)`: Creates a new tool group with the specified ID.
- `addToolsToToolGroup(toolGroupId, tools, configs?)`: Adds tools to the specified tool group with optional configurations.
- `createToolGroupAndAddTools(toolGroupId, tools)`: Creates a new tool group and adds the specified tools to it.
- `getToolConfiguration(toolGroupId, toolName)`: Retrieves the configuration for the specified tool in the given tool group.
- `setToolConfiguration(toolGroupId, toolName, config)`: Sets the configuration for the specified tool in the given tool group.
## Usage
Here's an example of how to create a new tool group and add tools to it in our basic viewer mode (modes/longitudinal/src/initToolGroups.js)
```js
import { initToolGroups } from '@ohif/extension-cornerstone';
import { ToolGroupService } from '@ohif/core';
const toolGroupService = new ToolGroupService();
// Create a new tool group
const defaultToolGroup = toolGroupService.createToolGroup('default');
// Define tools for the tool group
const tools = {
active: [
{ toolName: 'WindowLevel', bindings: [{ mouseButton: 1 }] },
{ toolName: 'Pan', bindings: [{ mouseButton: 2 }] },
{ toolName: 'Zoom', bindings: [{ mouseButton: 3 }] },
],
passive: [
{ toolName: 'Length' },
{ toolName: 'ArrowAnnotate' },
{ toolName: 'Bidirectional' },
],
};
// Add tools to the tool group
toolGroupService.addToolsToToolGroup('default', tools);
```
In this example, we create a new `ToolGroupService` instance and use it to create a new tool group with the ID `'default'`. We then define an object `tools` that contains the active and passive tools we want to add to the tool group. Finally, we call the `addToolsToToolGroup` method to add the tools to the newly created tool group.
:::tip
You can begin the viewer with certain toggle tools already active. For example, if you have your 'referencelines' tool enabled, it will be active when the viewer starts, and the icon state will be correctly set to active as well.
:::
```js
const tools = {
// the reset
// enabled
enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }],
};
```
![alt text](../../../assets/img/reference-lines-from-start.png)
@@ -0,0 +1,266 @@
---
sidebar_position: 5
sidebar_label: Toolbar Service
---
# Toolbar **Service**
## Overview
The `ToolBarService` is a straightforward service designed to handle the toolbar. Its main tasks include adding buttons, configuring them, and organizing button sections. When a button is clicked, it executes the designated commands. In the past, this service was more intricate, managing button states and logic. However, all that functionality has now been transferred to the `ToolBarModule` and evaluators.
<!-- <div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/547957214?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} **title**="measurement-report"></iframe>
</div> -->
## Events
| Event | Description |
| ----------------------- | ---------------------------------------------------------------------- |
| TOOL_BAR_MODIFIED | Fires when a button is added/removed to the toolbar |
| TOOL_BAR_STATE_MODIFIED | Fires when an interaction happens and ToolBarService state is modified |
## API
- `createButtonSection(key, buttons)` : creates a section of buttons in the toolbar with the given key and button Ids
- `addButtons`: add the button definition to the service.
[See below for button definition](#button-definitions).
- `removeButton(key)` : remove a button from the toolbar.
- `setButtons`: sets the buttons defined in the service. It overrides all the
previous buttons
## Button Definitions
### Basic
The simplest toolbarButtons definition has the following properties:
![toolbarModule-zoom](../../../assets/img/toolbarModule-zoom.png)
```js
{
id: 'Zoom',
uiType: 'ohif.radioGroup',
props: {
icon: 'tool-zoom',
label: 'Zoom',
"commands": [
{
"commandName": "setToolActive",
"commandOptions": {
"toolName": "Zoom"
},
"context": "CORNERSTONE"
}
]
evaluate: 'evaluate.cornerstoneTool',
},
},
```
| property | description | values |
| ---------------- | ----------------------------------------------------------------- | ------------------------------------------- |
| `id` | Unique string identifier for the definition | \* |
| `icon` | A string name for an icon supported by the consuming application. | \* |
| `label` | User/display friendly to show in UI | \* |
| `commands` | (optional) The commands to run when the button is used. It include a commandName, commandOptions, and/or a context | Any command registered by a `CommandModule` |
### Nested (dropdown)
You can use the `ohif.splitButton` type to build a button with extra tools in
the dropdown.
- First you need to give your `primary` tool definition to the split button
- the `secondary` properties can be a simple arrow down (`chevron-down` icon)
- For adding the extra tools add them to the `items` list.
You can see below how `longitudinal` mode is using the available toolbarModule
to create `MeasurementTools` nested button
![toolbarModule-nested-buttons](../../../assets/img/toolbarModule-nested-buttons.png)
```js title="modes/longitudinal/src/toolbarButtons.js"
{
id: 'MeasurementTools',
uiType: 'ohif.splitButton',
props: {
groupId: 'MeasurementToolsGroupId',
// group evaluate to determine which item should move to the top
evaluate: 'evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList',
primary: ToolbarService.createButton({
id: 'Length',
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: _createSetToolActiveCommands('Length'),
evaluate: 'evaluate.cornerstoneTool',
}),
secondary: {
icon: 'chevron-down',
tooltip: 'More Measure Tools',
},
items: [
ToolbarService.createButton({
id: 'Length',
icon: 'tool-length',
label: 'Length',
tooltip: 'Length Tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Length',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRLength',
toolGroupId: 'SRToolGroup',
},
// we can use the setToolActive command for this from Cornerstone commandsModule
context: 'CORNERSTONE',
},
],
evaluate: 'evaluate.cornerstoneTool',
}),
ToolbarService.createButton({
id: 'Bidirectional',
icon: 'tool-bidirectional',
label: 'Bidirectional',
tooltip: 'Bidirectional Tool',
commands: [
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'Bidirectional',
},
context: 'CORNERSTONE',
},
{
commandName: 'setToolActive',
commandOptions: {
toolName: 'SRBidirectional',
toolGroupId: 'SRToolGroup',
},
context: 'CORNERSTONE',
},
],
evaluate: 'evaluate.cornerstoneTool',
}),
],
},
},
```
:::tip
split buttons can have a group evaluator (in the above example `evaluate.group.promoteToPrimaryIfCornerstoneToolNotActiveInTheList`) which can decide what happens
when the user interacts with the buttons. In the above example, we are promoting the button to the primary section if the cornerstone tool is not active in the list of buttons.
There are other evaluators for instance `evaluate.group.promoteToPrimary`
which does not care about the cornerstone tool and promotes the button to the primary section anyway
:::
:::tip
If you don't provide a group evaluator nothing would happen and the button will stay in the secondary section.
:::
## Listeners
Sometimes you need a tool to listen to specific events in order to react properly.
You can add `listeners` for this purpose. We use this pattern for referencelineTools
which should set its source of reference upon active viewport change
Currently you can subscribe to the following events:
- `ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED`: when the active viewport changes
- `ViewportGridService.EVENTS.VIEWPORTS_READY`: when the viewports are ready in the grid
```js
const ReferenceLinesListeners: RunCommand = [
{
commandName: 'setSourceViewportForReferenceLinesTool',
context: 'CORNERSTONE',
},
];
ToolbarService.createButton({
id: 'ReferenceLines',
icon: 'tool-referenceLines',
label: 'Reference Lines',
tooltip: 'Show Reference Lines',
commands: [
{
commandName: 'setToolEnabled',
commandOptions: {
toolName: 'ReferenceLines',
toggle: true,
},
context: 'CORNERSTONE',
},
],
listeners: {
[ViewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED]: ReferenceLinesListeners,
[ViewportGridService.EVENTS.VIEWPORTS_READY]: ReferenceLinesListeners,
},
evaluate: 'evaluate.cornerstoneTool.toggle',
}),
```
## Button Sections
In order to organize the buttons, you can create button sections in the toolbar. And
assign buttons to each section separately.
OHIF provides a `primary` section by default. You can add more sections as needed in your UI
and use toolbarService to create and manage them. (You can look at the toolBox implementation
which take advantage of having a dedicated section for the tools with advanced options,
we use that in the segmentation mode).
## Example
For instance in `longitudinal` mode we are using the `onModeEnter` hook to
add the buttons to the toolbarService and assign them to the primary section.
```js title="modes/longitudinal/src/index.js"
toolbarService.addButtons([...toolbarButtons, ...moreTools]);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'info',
'WindowLevel',
'Pan',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
]);
```
as you see we creating the button section and assigning buttons based on their Ids.
:::tip
You can even duplicate the same button in different sections and the button will be
in sync in all sections (thanks to the evaluation system).
:::
:::tip
we will add more section in the toolbar (other than primary) in the future.
:::
:::note
Don't forget to set up your toolGroups to ensure that your buttons function correctly. Buttons serve as a visual interface. When you interact with them, they execute their commands, and evaluators determine their state post-interaction.
:::
@@ -0,0 +1,174 @@
---
sidebar_position: 9
sidebar_label: WorkflowStep Service
---
# Workflow Step Service
This service allows you to manage your workflow in smaller steps. It provides a structured way to define and navigate through different stages
or phases of a larger process or workflow. Each step can have its own configuration, layout, toolbar buttons, and other settings tailored to the specific requirements of that stage.
## Anatomy of a Workflow Step
The anatomy of a workflow step refers to the different components or properties that define and configure each individual step within the workflow. Each step can be customized with various settings to tailor the user interface, available tools, and behavior of the application for that specific stage of the workflow. Here are the key components that make up a workflow step:
- `id`: A unique identifier for the step
- `name`: A human-readable name or title for the step, which can be displayed in the user interface to help users understand the current stage of the workflow.
- `hangingProtocol`: The hanging protocol configuration specifies the protocol and stage ID to be used for displaying the images. This ensures that the appropriate data viewports and presentation are used for the current workflow step.
- `layout`: The layout configuration defines the arrangement and visibility of various panels or viewports within the application's user interface for the specific step. This can include specifying which panels should be visible on the left or right side of the screen, as well as any options for panel visibility or behavior.
- `toolbarButtons`: Each step can define a set of toolbar buttons that should be available and displayed in the application's toolbar during that step. Remember the button definitions should already be registered to toolbarService beforehand, here we are just referencing the buttons id in each section.
- `info` : An optional description or additional information about the current workflow step can be provided. which
will be displayed as tooltip in the UI.
- Step Callbacks or Commands: Some workflow steps may require specific actions or commands to be executed when the step is entered or exited. These callbacks or commands can be defined within the step configuration and can be used to update the application's state, perform data processing, or trigger other relevant actions. For instance you have access to `onEnter` hook to run a command right after the step is entered.
For instance, a simplified example of our pre-clinical 4D workflow steps configuration might look like this:
```js
const dynamicVolume = {
sopClassHandler:
"@ohif/extension-cornerstone-dynamic-volume.sopClassHandlerModule.dynamic-volume",
leftPanel:
"@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-volume",
toolBox:
"@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-toolbox",
export:
"@ohif/extension-cornerstone-dynamic-volume.panelModule.dynamic-export",
}
const cs3d = {
segmentation:
"@ohif/extension-cornerstone-dicom-seg.panelModule.panelSegmentation",
}
const steps = [
{
id: "dataPreparation",
name: "Data Preparation",
layout: {
panels: {
left: [dynamicVolume.leftPanel],
},
},
toolbarButtons: {
buttonSection: "primary",
buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"],
},
hangingProtocol: {
protocolId: "default4D",
stageId: "dataPreparation",
},
info: "In the Data Preparation step...",
},
{
id: "roiQuantification",
name: "ROI Quantification",
layout: {
panels: {
left: [dynamicVolume.leftPanel],
right: [
[dynamicVolume.toolBox, cs3d.segmentation, dynamicVolume.export],
],
},
options: {
leftPanelClosed: false,
rightPanelClosed: false,
},
},
toolbarButtons: [
{
buttonSection: "primary",
buttons: [
"MeasurementTools",
"Zoom",
"WindowLevel",
"Crosshairs",
"Pan",
],
},
{
buttonSection: "dynamic-toolbox",
buttons: ["BrushTools", "RectangleROIStartEndThreshold"],
},
],
hangingProtocol: {
protocolId: "default4D",
stageId: "roiQuantification",
},
info: "The ROI quantification step ...",
},
{
id: "kineticAnalysis",
name: "Kinetic Analysis",
layout: {
panels: {
left: [dynamicVolume.leftPanel],
right: [],
},
},
toolbarButtons: {
buttonSection: "primary",
buttons: ["MeasurementTools", "Zoom", "WindowLevel", "Crosshairs", "Pan"],
},
hangingProtocol: {
protocolId: "default4D",
stageId: "kineticAnalysis",
},
onEnter: [
{
commandName: "updateSegmentationsChartDisplaySet",
options: { servicesManager },
},
],
info: "The Kinetic Analysis step ...",
},
]
```
## Integration
After you have defined your workflow steps, you can integrate them into your application by using the `workflowStepsService`.
These steps should be called on `onSetupRouteComplete` in your mode factory.
Note: onModeEnter is too soon to call these steps as the mode is not yet fully initialized.
```js
onSetupRouteComplete: ({ servicesManager }) => {
workflowStepsService.addWorkflowSteps(workflowSettings.steps);
workflowStepsService.setActiveWorkflowStep(workflowSettings.steps[0].id);
},
```
check out the `modes/preclinical-4d/src/index.tsx` for a complete example.
## User Interface
We have developed a simple dropdown UI element that you can use to navigate between the different steps of your workflow. This dropdown can be added to the toolbar like below:
```js
toolbarService.addButtons([
{
id: 'ProgressDropdown',
uiType: 'ohif.progressDropdown',
},
])
toolbarService.createButtonSection('secondary', ['ProgressDropdown']);
```
It will appear in the `secondary` location in the toolbar.
![alt text](../../../assets/img/progressDropdown.png)
:::note
if you like to place the progressbar in a different location, you can use the Toolbox component
to create a button section and place the progress bar there.
Read more in the [Toolbar module](../../extensions//modules/toolbar.md)
:::
@@ -0,0 +1,4 @@
{
"label": "Data Services",
"position": 2
}
@@ -0,0 +1,55 @@
---
sidebar_position: 1
sidebar_label: Overview
---
# Overview
Data services are the first category of services which deal with handling non-ui
related state Each service have their own internal state which they handle.
> We have replaced the _redux_ store. Instead, we have introduced various
> services and a pub/sub pattern to subscribe and run, which makes the `OHIF-v3`
> architecture nice and clean.
We maintain the following non-ui Services:
- [DicomMetadata Store](./../data/DicomMetadataStore.md)
- [DisplaySet Service](./../data/DisplaySetService.md)
- [Hanging Protocol Service](../data/HangingProtocolService.md)
- [Toolbar Service](./ToolbarService.md)
- [Measurement Service](../data/MeasurementService.md)
- [Customization Service](./../customization-service/customizationService.md)
- [Panel Service](../data/PanelService.md)
## Service Architecture
![services-data](../../../assets/img/services-data.png)
> We have explained services and how to create a custom service in the
> [`ServicesManager`](../../managers/service.md) section of the docs
To recap: The simplest service return a new object that has a `name` property,
and `Create` method which instantiate the service class. The "Factory Function"
that creates the service is provided with the implementation (this is slightly
different for UI Services).
```js
// extensions/customExtension/src/services/backEndService/index.js
import backEndService from './backEndService';
export default function WrappedBackEndService(servicesManager) {
return {
name: 'myService',
create: ({ configuration = {} }) => {
return new backEndService(servicesManager);
},
};
}
```
A service, once created, can be registered with the `ServicesManager` to make it
accessible to extensions. Similarly, the application code can access named
services from the `ServicesManager`.
[Read more of how to design a new custom service and register it](../../managers/service.md)
@@ -0,0 +1,195 @@
---
sidebar_position: 1
sidebar_label: Introduction
---
# Services
## Overview
Services are "concern-specific" code modules that can be consumed across layers.
Services provide a set of operations, often tied to some shared state, and are
made available to through out the app via the `ServicesManager`. Services are
particularly well suited to address [cross-cutting
concerns][cross-cutting-concerns].
Each service should be:
- self-contained
- able to fail and/or be removed without breaking the application
- completely interchangeable with another module implementing the same interface
> In `OHIF-v3` we have added multiple non-UI services and have introduced
> **pub/sub** pattern to reduce coupling between layers.
>
> [Read more about Pub/Sub](./pubsub.md)
## Services
The following services is available in the `OHIF-v3`.
<table>
<thead>
<tr>
<th>Service</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="./data/DicomMetadataStore">
DicomMetadataStore
</a>
</td>
<td>Data Service</td>
<td>
DicomMetadataStore
</td>
</tr>
<tr>
<td>
<a href="./data/DisplaySetService">
DisplaySetService
</a>
</td>
<td>Data Service</td>
<td>
DisplaySetService
</td>
</tr>
<tr>
<td>
<a href="./data/SegmentationService">
segmentationService
</a>
</td>
<td>Segmentation Service</td>
<td>
segmentationService
</td>
</tr>
<tr>
<td>
<a href="./data/HangingProtocolService">
HangingProtocolService
</a>
</td>
<td>Data Service</td>
<td>
HangingProtocolService
</td>
</tr>
<tr>
<td>
<a href="./data/MeasurementService">
MeasurementService (MODIFIED)
</a>
</td>
<td>Data Service</td>
<td>
MeasurementService
</td>
</tr>
<tr>
<td>
<a href="./data/ToolBarService">
ToolBarService
</a>
</td>
<td>Data Service</td>
<td>
ToolBarService
</td>
</tr>
<tr>
<td>
<a href="./ui/viewport-grid-service">
ViewportGridService
</a>
</td>
<td>UI Service</td>
<td>
ViewportGridService
</td>
</tr>
<tr>
<td>
<a href="./ui/cine-service">
Cine Service
</a>
</td>
<td>UI Service</td>
<td>
cine
</td>
</tr>
<tr>
<td>
<a href="./customization-service">
CustomizationService
</a>
</td>
<td>UI Service</td>
<td>
customizationService
</td>
</tr>
<tr>
<td>
<a href="./ui/ui-dialog-service">
UIDialogService
</a>
</td>
<td>UI Service</td>
<td>
UIDialogService
</td>
</tr>
<tr>
<td>
<a href="./ui/ui-modal-service">
UIModalService
</a>
</td>
<td>UI Service</td>
<td>
UIModalService
</td>
</tr>
<tr>
<td>
<a href="./ui/ui-notification-service">
UINotificationService
</a>
</td>
<td>UI Service</td>
<td>
UINotificationService
</td>
</tr>
<tr>
<td>
<a href="./ui/ui-viewport-dialog-service">
UIViewportDialogService
</a>
</td>
<td>UI Service</td>
<td>
UIViewportDialogService
</td>
</tr>
</tbody>
</table>
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
<!-- prettier-ignore-end -->
@@ -0,0 +1,114 @@
---
sidebar_position: 4
sidebar_label: Pub Sub
---
# Pub sub
## Overview
Publish–subscribe pattern is a messaging pattern that is one of the fundamentals
patterns used in reusable software components.
In short, services that implement this pattern, can have listeners subscribed
to their broadcasted events. After the event is fired, the corresponding
listener will execute the function that is registered.
You can read more about this design pattern
[here](https://cloud.google.com/pubsub/docs/overview).
## Example: Default Initialization
In `Mode.jsx` we have a default initialization that demonstrates a series of
subscriptions to various events.
```js
async function defaultRouteInit({
servicesManager,
studyInstanceUIDs,
dataSource,
}) {
const {
DisplaySetService,
HangingProtocolService,
} = servicesManager.services;
const unsubscriptions = [];
const {
unsubscribe: instanceAddedUnsubscribe,
} = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
const seriesMetadata = DicomMetadataStore.getSeries(
StudyInstanceUID,
SeriesInstanceUID
);
DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
}
);
unsubscriptions.push(instanceAddedUnsubscribe);
studyInstanceUIDs.forEach(StudyInstanceUID => {
dataSource.retrieve.series.metadata({ StudyInstanceUID });
});
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
({ StudyInstanceUID }) => {
HangingProtocolService.run({studies, displaySets, activeStudy});
}
);
unsubscriptions.push(seriesAddedUnsubscribe);
return unsubscriptions;
}
```
## Unsubscription
You need to be careful if you are adding custom subscriptions to the app. Each
subscription will return an unsubscription function that needs to be executed on
component destruction to avoid adding multiple subscriptions to the same
observer.
Below, we can see `simplified` `Mode.jsx` and the corresponding `useEffect`
where the unsubscription functions are executed upon destruction.
```js title="platform/app/src/routes/Mode/Mode.jsx"
export default function ModeRoute(/**..**/) {
/**...**/
useEffect(() => {
/**...**/
DisplaySetService.init(extensionManager, sopClassHandlers);
extensionManager.onModeEnter();
mode?.onModeEnter({ servicesManager, extensionManager });
const setupRouteInit = async () => {
if (route.init) {
return await route.init(/**...**/);
}
return await defaultRouteInit(/**...**/);
};
let unsubscriptions;
setupRouteInit().then(unsubs => {
unsubscriptions = unsubs;
});
return () => {
extensionManager.onModeExit();
mode?.onModeExit({ servicesManager, extensionManager });
unsubscriptions.forEach(unsub => {
unsub();
});
};
});
return <> /**...**/ </>;
}
```
@@ -0,0 +1,4 @@
{
"label": "UI Services",
"position": 3
}
@@ -0,0 +1,8 @@
---
sidebar_position: 7
sidebar_label: CINE Service
---
# CINE Service
TODO...
@@ -0,0 +1,304 @@
---
sidebar_position: 1
sidebar_label: Overview
---
# Overview
<!-- - Consistent look & feel
- Available in and “out” of React
- Supports ad hoc containers
- Exposes “setImplementation” method passed to Context Provider
- May set a default UI component
- API exposes get/set of Context Provider’s state -->
A typical web application will have components and state for common UI like
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
these components from an extension.
We maintain the following UI Services:
- [UI Notification Service](ui-notification-service.md)
- [UI Modal Service](ui-modal-service.md)
- [UI Dialog Service](ui-dialog-service.md)
- [UI Viewport Dialog Service](ui-viewport-dialog-service.md)
- [CINE Service](cine-service.md)
- [Viewport Grid Service](viewport-grid-service.md)
<!-- <div style="text-align: center;">
<a href="/assets/img/ui-services.png">
<img src="/assets/img/ui-services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
</div> -->
![UIService](../../../assets/img/ui-services.png)
<!-- In `@ohif/core`, we have a collection of service factories. We select one we
would like our application to support, create an instance of it, and pass that
instance to our `ServicesManager` AND to a React component (in this example,
`ModalContext`'s provider).
The `ModalContext`'s provider:
- Exposes context values
- Exposes methods that leverage `useCallback` hooks
- Sets the service's implementation in a `useEffect` hook
The `ServicesManager` is:
- Passed to the `ExtensionManager`
- The `ExtensionManager` makes the `ServicesManager` available to:
- All of its lifecycle hooks (`preRegistration`)
- Each "getModuleFunction" (`getToolbarModule`, `getPanelModule`, etc.) -->
## Providers for UI services
**There are several context providers that wraps the application routes. This
makes the context values exposed in the app, and service's `setImplementation`
can get run to override the implementation of the service.**
```js title="platform/app/src/App.jsx"
function App({ config, defaultExtensions }) {
/**...**/
/**...**/
return (
/**...**/
<ViewportGridProvider service={ViewportGridService}>
<ViewportDialogProvider service={UIViewportDialogService}>
<CineProvider service={CineService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</CineProvider>
</ViewportDialogProvider>
</ViewportGridProvider>
/**...**/
);
}
```
## Example
For instance `UIModalService` has the following Public API:
```js title="platform/core/src/services/UIModalService/index.js"
const publicAPI = {
name,
hide: _hide,
show: _show,
setServiceImplementation,
};
function setServiceImplementation({
hide: hideImplementation,
show: showImplementation,
}) {
/** ... **/
serviceImplementation._hide = hideImplementation;
serviceImplementation._show = showImplementation;
/** ... **/
}
export default {
name: 'UIModalService',
create: ({ configuration = {} }) => {
return publicAPI;
},
};
```
`UIModalService` implementation can be set (override) in its context provider.
For instance in `ModalProvider` we have:
```js title="platform/ui/src/contextProviders/ModalProvider.jsx"
import { Modal } from '@ohif/ui';
const ModalContext = createContext(null);
const { Provider } = ModalContext;
export const useModal = () => useContext(ModalContext);
const ModalProvider = ({ children, modal: Modal, service }) => {
const DEFAULT_OPTIONS = {
content: null,
contentProps: null,
shouldCloseOnEsc: true,
isOpen: true,
closeButton: true,
title: null,
customClassName: '',
};
const show = useCallback(props => setOptions({ ...options, ...props }), [
options,
]);
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [
DEFAULT_OPTIONS,
]);
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [hide, service, show]);
const {
content: ModalContent,
contentProps,
isOpen,
title,
customClassName,
shouldCloseOnEsc,
closeButton,
} = options;
return (
<Provider value={{ show, hide }}>
{ModalContent && (
<Modal
className={classNames(customClassName, ModalContent.className)}
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
closeButton={closeButton}
onClose={hide}
>
<ModalContent {...contentProps} show={show} hide={hide} />
</Modal>
)}
{children}
</Provider>
);
};
export default ModalProvider;
export const ModalConsumer = ModalContext.Consumer;
```
Therefore, anywhere in the app that we have access to react context we can use
it by calling the `useModal` from `@ohif/ui`. As a matter of fact, we are
utilizing the modal for the preference window which shows the hotkeys after
clicking on the gear button on the right side of the header.
A `simplified` code for our worklist is:
```js title="platform/app/src/routes/WorkList/WorkList.jsx"
import { useModal, Header } from '@ohif/ui';
function WorkList({
history,
data: studies,
dataTotal: studiesTotal,
isLoadingData,
dataSource,
hotkeysManager,
}) {
const { show, hide } = useModal();
/** ... **/
const menuOptions = [
{
title: t('Header:About'),
icon: 'info',
onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }),
},
{
title: t('Header:Preferences'),
icon: 'settings',
onClick: () =>
show({
title: t('UserPreferencesModal:User Preferences'),
content: UserPreferences,
contentProps: {
hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions(
hotkeyDefaults
),
hotkeyDefinitions,
onCancel: hide,
currentLanguage: currentLanguage(),
availableLanguages,
defaultLanguage,
onSubmit: state => {
i18n.changeLanguage(state.language.value);
hotkeysManager.setHotkeys(state.hotkeyDefinitions);
hide();
},
onReset: () => hotkeysManager.restoreDefaultBindings(),
},
}),
},
];
/** ... **/
return (
<div>
/** ... **/
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} />
/** ... **/
</div>
);
}
```
<!-- ## Example -->
<!-- That's all fine and good, but it's still a little too abstract. What does this
translate to in practice?
```js
// In the application
import UINotificationService from '@ohif/core';
const servicesManager = new ServicesManager();
servicesManager.registerService(UINotificationService);
// UI Service Provider
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [service, hide, show]);
// In an extension
const { UINotificationService } = servicesManager.services;
if (UINotificationService) {
UINotificationService.show('Hello from the other side 👋');
}
``` -->
<!-- <div style="text-align: center;">
<a href="/assets/img/notification-example.gif">
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
</div> -->
## Tips & Tricks
It's important to remember that all we're doing is making it possible to control
bits of the application's UI from an extension. Here are a few non-obvious
takeaways worth mentioning:
- Your application code should continue to use React context
(consumers/providers) as it normally would
- You can substitute our "out of the box" UI implementations with your own
- You can create and register your own UI services
- You can choose not to register a service or provide a service implementation
- In extensions, you can provide fallback/alternative behavior if an expected
service is not registered
- No `UIModalService`? Use the `UINotificationService` to notify users.
- You can technically register a service in an extension and expose it to the
core application
> Note: These are recommended patterns, not hard and fast rules. Following them
> will help reduce confusion and interoperability with the larger OHIF
> community, but they're not silver bullets. Please speak up, create an issue,
> if you would like to discuss new services or improvements to this pattern.
@@ -0,0 +1,48 @@
---
sidebar_position: 4
sidebar_label: UI Dialog Service
---
# UI Dialog Service
Dialogs have similar characteristics to that of Modals, but often with a
streamlined focus. They can be helpful when:
- We need to grab the user's attention
- We need user input
- We need to show additional information
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| -------------- | ------------------------------------------------------ |
| `create()` | Creates a new Dialog that is displayed until dismissed |
| `dismiss()` | Dismisses the specified dialog |
| `dismissAll()` | Dismisses all dialogs |
## Implementations
| Implementation | Consumer |
| ------------------------------------ | -------------------------- |
| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider |
`*` - Denotes maintained by OHIF
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js
[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
<!-- prettier-ignore-end -->
@@ -0,0 +1,64 @@
---
sidebar_position: 3
sidebar_label: UI Modal Service
---
# UI Modal Service
Modals have similar characteristics to that of Dialogs, but are often larger,
and only allow for a single instance to be viewable at once. They also tend to
be centered, and not draggable. They're commonly used when:
- We need to grab the user's attention
- We need user input
- We need to show additional information
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/843233754?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| ---------- | ------------------------------------- |
| `hide()` | Hides the open modal |
| `show()` | Shows the provided content in a modal |
| `customComponent()` | Overrides the default Modal component |
## Implementations
| Implementation | Consumer |
| ---------------------------------- | --------- |
| [Modal Provider][modal-provider]\* | Modal.jsx |
| customComponent | user extensions via `setServiceImplementation({customComponent: Modal})` |
### Custom Component
If you would like to customize the modal component that OHIF uses, you can register your own
component with the `customComponent` property.
```js
setServiceImplementation({customComponent: Modal})
```
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
<!-- prettier-ignore-end -->
@@ -0,0 +1,55 @@
---
sidebar_position: 2
sidebar_label: UI Notification Service
---
# UI Notification Service
Notifications can be annoying and disruptive. They can also deliver timely
helpful information, or expedite the user's workflow. Here is some high level
guidance on when and how to use them:
- Notifications should be non-interfering (timely, relevant, important)
- We should only show small/brief notifications
- Notifications should be contextual to current behavior/actions
- Notifications can serve warnings (acting as a confirmation)
If you're curious about the DOs and DON'Ts of notifications, check out this
article: ["How To Design Notifications For Better UX"][ux-article]
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/843233715?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| ---------- | --------------------------------------- |
| `hide()` | Hides the specified notification |
| `show()` | Creates and displays a new notification |
## Implementations
| Implementation | Consumer |
| ---------------------------------------- | ----------------------------------------- |
| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* |
`*` - Denotes maintained by OHIF
> 3rd Party implementers may be added to this table via pull requests.
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js
[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js
[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js
[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d
<!-- prettier-ignore-end -->
@@ -0,0 +1,65 @@
---
sidebar_position: 5
sidebar_label: UI Viewport Dialog Service
---
# UI Viewport Dialog Service
## Overview
This is a new UI service, that creates a modal inside the viewport.
Dialogs have similar characteristics to that of Modals, but often with a
streamlined focus. They can be helpful when:
- We need to grab the user's attention
- We need user input
- We need to show additional information
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
<div style={{padding:"56.25% 0 0 0", position:"relative"}}>
<iframe src="https://player.vimeo.com/video/549261939?badge=0&amp;autopause=0&amp;player_id=0&amp;app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="measurement-report"></iframe>
</div>
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| -------------- | ------------------------------------------------------ |
| `create()` | Creates a new Dialog that is displayed until dismissed |
| `dismiss()` | Dismisses the specified dialog |
| `dismissAll()` | Dismisses all dialogs |
## Implementations
| Implementation | Consumer |
| ------------------------ | -------------------------- |
| [ViewportDialogProvider] | Baked into Dialog Provider |
`*` - Denotes maintained by OHIF
## State
```js
const DEFAULT_STATE = {
viewportId: null,
message: undefined,
type: 'info', // "error" | "warning" | "info" | "success"
actions: undefined, // array of { type, text, value }
onSubmit: () => {
console.log('btn value?');
},
onOutsideClick: () => {
console.warn('default: onOutsideClick')
},
onDismiss: () => {
console.log('dismiss? -1');
},
};
```
@@ -0,0 +1,30 @@
---
sidebar_position: 8
sidebar_label: Viewport Action Corners
---
# Viewport Action Corners Service
The Viewport Action Corners Service is a powerful tool for managing interactive components in the corners of viewports within the OHIF viewer. This service allows developers to dynamically add, remove, and organize various UI elements such as menus, buttons, or custom components in specific locations around the viewport.
## Overview
The Viewport Action Corners Service extends the PubSubService and provides methods to:
- Add single or multiple components to viewport corners
- Clear components from a specific viewport
- Manage the state of viewport corner components
## Key Features
- **Flexible Positioning**: Components can be placed in top-left, top-right, bottom-left, or bottom-right corners of the viewport.
- **Priority Ordering**: Components can be assigned priority indices for ordering within a corner.
- **Viewport-Specific**: Actions are associated with specific viewports, allowing for individualized control.
- **Dynamic Updates**: Components can be added or removed at runtime, enabling context-sensitive UI elements.
## Usage
To use the Viewport Action Corners Service, you typically interact with it through the `servicesManager`. Here's a basic example of how to add a component:
Take a look at how we add window level menu to the top right corner of the viewport in the `OHIFCornerstoneViewport` component.
@@ -0,0 +1,65 @@
---
sidebar_position: 6
sidebar_label: Viewport Grid Service
---
# Viewport Grid Service
## Overview
This is a new UI service, that handles the grid layout of the viewer.
## Events
There are seven events that get publish in `ViewportGridService `:
| Event | Description |
| ----------------------------- | --------------------------------------------------|
| ACTIVE_VIEWPORT_ID_CHANGED | Fires the Id of the active viewport is changed |
| LAYOUT_CHANGED | Fires the layout is changed |
| GRID_STATE_CHANGED | Fires when the entire grid state is changed |
| VIEWPORTS_READY | Fires when the viewports are ready in the grid |
## Interface
For a more detailed look on the options and return values each of these methods
is expected to support, [check out it's interface in `@ohif/core`][interface]
| API Member | Description |
| --------------------------------------------------------------------- | --------------------------------------------------- |
| `setActiveViewportId(viewportId)` | Sets the active viewport Id in the app |
| `getState()` | Gets the states of the viewport (see below) |
| `setDisplaySetsForViewport({ viewportId, displaySetInstanceUID })` | Sets displaySet for viewport based on displaySet Id |
| `setLayout({numCols, numRows, keepExtraViewports})` | Sets rows and columns. When the total number of viewports decreases, optionally keep the extra/offscreen viewports. |
| `reset()` | Resets the default states |
| `getNumViewportPanes()` | Gets the number of visible viewport panes |
| `getLayoutOptionsFromState(gridState)` | Utility method that produces a `ViewportLayoutOptions` based on the passed in state|
| `getActiveViewportId()` | Returns the viewport Id of the active viewport in the grid|
| `getActiveViewportOptionByKey(key)` | Gets the specified viewport option field (key) for the active viewport |
## Implementations
| Implementation | Consumer |
| ---------------------- | -------------------------- |
| [ViewportGridProvider] | Baked into Dialog Provider |
`*` - Denotes maintained by OHIF
## State
```js
const DEFAULT_STATE = {
// starting from null, hanging
// protocol will defined number of rows and cols
numRows: null,
numCols: null,
viewports: [
/*
* {
* displaySetInstanceUID: string,
* }
*/
],
activeViewportId: null,
};
```
@@ -0,0 +1,166 @@
---
sidebar_position: 2
sidebar_label: Theming
---
# Viewer: Theming
`OHIF-v3` has introduced the
[`LayoutTemplateModule`](./extensions/modules/layout-template.md) which enables
addition of custom layouts. You can easily design your custom components inside
an extension and consume it via the layoutTemplate module you write.
## Tailwind CSS
[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for
creating custom user interfaces.
Below you can see a compiled version of the tailwind configs. Each section can
be edited accordingly. For instance screen size break points, primary and
secondary colors, etc.
```js
module.exports = {
prefix: '',
important: false,
separator: ':',
theme: {
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
colors: {
overlay: 'rgba(0, 0, 0, 0.8)',
transparent: 'transparent',
black: '#000',
white: '#fff',
initial: 'initial',
inherit: 'inherit',
indigo: {
dark: '#0b1a42',
},
aqua: {
pale: '#7bb2ce',
},
primary: {
light: '#5acce6',
main: '#0944b3',
dark: '#090c29',
active: '#348cfd',
},
secondary: {
light: '#3a3f99',
main: '#2b166b',
dark: '#041c4a',
active: '#1f1f27',
},
common: {
bright: '#e1e1e1',
light: '#a19fad',
main: '#fff',
dark: '#726f7e',
active: '#2c3074',
},
customgreen: {
100: '#05D97C',
},
customblue: {
100: '#c4fdff',
200: '#38daff',
},
},
},
};
```
You can also use the color variable like before. For instance:
```js
primary: {
default: ‘var(--default-color)‘,
light: ‘#5ACCE6’,
main: ‘#0944B3’,
dark: ‘#090C29’,
active: ‘#348CFD’,
}
```
## White Labeling
A white-label product is a product or service produced by one company (the
producer) that other companies (the marketers) rebrand to make it appear as if
they had made it -
[Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product)
Current white-labeling options are limited. We expose the ability to replace the
"Logo" section of the application with a custom "Logo" component. You can do
this by adding a whiteLabeling key to your configuration file.
```js
window.config = {
/** .. **/
whiteLabeling: {
createLogoComponentFn: function(React) {
return React.createElement(
'a',
{
target: '_blank',
rel: 'noopener noreferrer',
className: 'text-white underline',
href: 'http://radicalimaging.com',
},
React.createElement('h5', {}, 'RADICAL IMAGING')
);
},
},
/** .. **/
};
```
> You can simply use the stylings from tailwind CSS in the whiteLabeling
In addition to text, you can also add your custom logo
```js
window.config = {
/** .. **/
whiteLabeling: {
createLogoComponentFn: function(React) {
return React.createElement(
'a',
{
target: '_self',
rel: 'noopener noreferrer',
className: 'text-purple-600 line-through',
href: '/',
},
React.createElement('img', {
src: './customLogo.svg',
// className: 'w-8 h-8',
})
);
},
},
/** .. **/
};
```
The output will look like
![custom-logo](../assets/img/custom-logo.png)
<!--
Links
-->
<!-- prettier-ignore-start -->
[wikipedia]: https://en.wikipedia.org/wiki/White-label_product
<!-- prettier-ignore-end -->