chore(version): move to the next beta version [BUMP BETA] (#4939)
This commit is contained in:
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)
|
||||
+30
@@ -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
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
```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 | === | All members are === in same order | value = ['abc', 'def', 'GHI']<br/>testValue = 'abc' (Fail)<br/>= ['abc'] (Fail)<br/>= ['abc', 'def', 'GHI'] (Valid)<br/>= ['abc', 'GHI', 'def'] (Fail)<br/>= ['abc', 'def'] (Fail)<br/><br/>value = 'Attenuation Corrected'<br/>testValue = 'Attenuation Corrected' (Valid)<br/>= 'Attenuation' (Fail)<br/><br/>value = ['Attenuation Corrected']<br/>testValue = ['Attenuation Corrected'] (Valid)<br/>= 'Attenuation Corrected' (Valid)<br/>= 'Attenuation' (Fail) |
|
||||
| doesNotEqual | !== | Any member is !== for the array, either in value, order, or length | value = ['abc', 'def', 'GHI']<br/>testValue = 'abc' (Valid)<br/>= ['abc'] (Valid)<br/>= ['abc', 'def', 'GHI'] (Fail)<br/>= ['abc', 'GHI', 'def'] (Valid)<br/>= ['abc', 'def'] (Valid)<br/><br/>value = 'Attenuation Corrected'<br/>testValue = 'Attenuation Corrected' (Fail)<br/>= 'Attenuation' (Valid)<br/><br/>value = ['Attenuation Corrected']<br/>testValue = ['Attenuation Corrected'] (Fail)<br/>= 'Attenuation Corrected' (Fail)<br/>= 'Attenuation' (Fail) |
|
||||
| includes | Not allowed | Value is equal to one of the values of the array | value = ['abc', 'def', 'GHI']<br/>testValue = ['abc'] (Valid)<br/>= 'abc' (Fail)<br/>= ['abc'] (Fail)<br/>= 'dog' (Fail)<br/>= ['att', 'abc'] (Valid)<br/>= ['abc', 'def', 'dog'] (Valid)<br/>= ['cat', 'dog'] (Fail)<br/><br/>value = 'Attenuation Corrected'<br/>testValue = ['Attenuation Corrected', 'Corrected'] (Valid)<br/>= ['Attenuation', 'Corrected'] (Fail)<br/><br/>value = ['Attenuation Corrected']<br/>testValue = 'Attenuation Corrected' (Fail)<br/>= ['Attenuation Corrected', 'Corrected'] (Valid)<br/>= ['Attenuation', 'Corrected'] (Fail) |
|
||||
| doesNotInclude | Not allowed | Value is not in one of the values of the array | value = ['abc', 'def', 'GHI']<br/>testValue = 'Corr' (Valid)<br/>= 'abc' (Fail)<br/>= ['att', 'cor'] (Valid)<br/>= ['abc', 'def', 'dog'] (Fail)<br/><br/>value = 'Attenuation Corrected'<br/>testValue = ['Attenuation Corrected', 'Corrected'] (Fail)<br/>= ['Attenuation', 'Corrected'] (Valid)<br/><br/>value = ['Attenuation Corrected']<br/>testValue = 'Attenuation' (Fail)<br/>= ['Attenuation Corrected', 'Corrected'] (Fail)<br/>= ['Attenuation', 'Corrected'] (Valid) |
|
||||
| containsI | String containment (case insensitive) | String containment (case insensitive) is OK for one of the rule values | value = 'Attenuation Corrected'<br/>testValue = 'Corr' (Valid)<br/>= 'corr' (Valid)<br/>= ['att', 'cor'] (Valid)<br/>= ['Att', 'Wall'] (Valid)<br/>= ['cat', 'dog'] (Fail)<br/><br/>value = ['abc', 'def', 'GHI']<br/>testValue = 'def' (Valid)<br/>= 'dog' (Fail)<br/>= ['gh', 'de'] (Valid)<br/>= ['cat', 'dog'] (Fail) |
|
||||
| contains | String containment (case sensitive) | String containment (case sensitive) is OK for one of the rule values | value = 'Attenuation Corrected'<br/>testValue = 'Corr' (Valid)<br/>= 'corr' (Fail)<br/>= ['att', 'cor'] (Fail)<br/>= ['Att', 'Wall'] (Valid)<br/>= ['cat', 'dog'] (Fail)<br/><br/>value = ['abc', 'def', 'GHI']<br/>testValue = 'def' (Valid)<br/>= 'dog' (Fail)<br/>= ['cat', 'de'] (Valid)<br/>= ['cat', 'dog'] (Fail) |
|
||||
| doesNotContain | String containment is false | String containment is false for all values of the array | value = 'Attenuation Corrected'<br/>testValue = 'Corr' (Fail)<br/>= 'corr' (Valid)<br/>= ['att', 'cor'] (Valid)<br/>= ['Att', 'Wall'] (Fail)<br/>= ['cat', 'dog'] (Valid)<br/><br/>value = ['abc', 'def', 'GHI']<br/>testValue = 'def' (Fail)<br/>= 'dog' (Valid)<br/>= ['cat', 'de'] (Fail)<br/>= ['cat', 'dog'] (Valid) |
|
||||
| doesNotContainI | String containment is false (case insensitive) | String containment (case insensitive) is false for all values of the array | value = 'Attenuation Corrected'<br/>testValue = 'Corr' (Fail)<br/>= 'corr' (Fail)<br/>= ['att', 'cor'] (Fail)<br/>= ['Att', 'Wall'] (Fail)<br/>= ['cat', 'dog'] (Valid)<br/><br/>value = ['abc', 'def', 'GHI']<br/>testValue = 'DEF' (Fail)<br/>= 'dog' (Valid)<br/>= ['cat', 'gh'] (Fail)<br/>= ['cat', 'dog'] (Valid) |
|
||||
| startsWith | Value begins with characters | Starts with one of the values of the array | value = 'Attenuation Corrected'<br/>testValue = 'Corr' (Fail)<br/>= 'Att' (Fail)<br/>= ['cat', 'dog', 'Att'] (Valid)<br/>= ['cat', 'dog'] (Fail)<br/><br/>value = ['abc', 'def', 'GHI']<br/>testValue = 'deg' (Valid)<br/>= ['cat', 'GH'] (Valid)<br/>= ['cat', 'gh'] (Fail)<br/>= ['cat', 'dog'] (Fail) |
|
||||
| endsWith | Value ends with characters | ends with one of the value of the array | value = 'Attenuation Corrected'<br/>testValue = 'TED' (Fail)<br/>= 'ted' (Valid)<br/>= ['cat', 'dog', 'ted'] (Valid)<br/>= ['cat', 'dog'] (Fail)<br/><br/>value = ['abc', 'def', 'GHI']<br/>testValue = 'deg' (Valid)<br/>= ['cat', 'HI'] (Valid)<br/>= ['cat', 'hi'] (Fail)<br/>= ['cat', 'dog'] (Fail) |
|
||||
| greaterThan | value is >= to rule | Not applicable | value = 30<br/>testValue = 20 (Valid)<br/>= 40 (Fail) |
|
||||
| lessThan | value is <= to rule | Not applicable | value = 30<br/>testValue = 40 (Valid)<br/>= 20 (Fail) |
|
||||
| range | Not applicable | 2 value requested (min and max) | value = 50<br/>testValue = [10,60] (Valid)<br/>= [60, 10] (Valid)<br/>= [0, 10] (Fail)<br/>= [70, 80] (Fail)<br/>= 45 (Fail)<br/>= [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
|
||||
};
|
||||
+139
@@ -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&autopause=0&player_id=0&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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
:::
|
||||
+120
@@ -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.
|
||||
|
||||
|
||||

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

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

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

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

|
||||
|
||||
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
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Measurements
|
||||
---
|
||||
|
||||
|
||||
|
||||
import { measurementsCustomizations, TableGenerator } from './sampleCustomizations';
|
||||
|
||||
{TableGenerator(measurementsCustomizations)}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Segmentation
|
||||
---
|
||||
|
||||
|
||||
|
||||
import { segmentationCustomizations, TableGenerator } from './sampleCustomizations';
|
||||
|
||||
{TableGenerator(segmentationCustomizations)}
|
||||
+11
@@ -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)}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"label": "Customization Service",
|
||||
"position": 4
|
||||
}
|
||||
+98
@@ -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.
|
||||
+21
@@ -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`.
|
||||
+83
@@ -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',
|
||||
],
|
||||
```
|
||||
+544
@@ -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)}
|
||||
+1949
File diff suppressed because it is too large.
Load diff
+21
@@ -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.
|
||||
|
||||

|
||||
|
||||
There are 4 viewport overlays customization end points
|
||||
|
||||
- `viewportOverlay.topRight`
|
||||
- `viewportOverlay.topLeft`
|
||||
- `viewportOverlay.bottomLeft`
|
||||
- `viewportOverlay.bottomRight`
|
||||
|
||||
|
||||
|
||||
{TableGenerator(viewportOverlayCustomizations)}
|
||||
+112
@@ -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.
|
||||
+367
@@ -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,
|
||||
},
|
||||
...
|
||||
```
|
||||
+182
@@ -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:
|
||||
|
||||

|
||||
|
||||
## 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' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
+71
@@ -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,
|
||||
],
|
||||
]);
|
||||
```
|
||||
+145
@@ -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 }],
|
||||
};
|
||||
```
|
||||
|
||||
|
||||

|
||||
@@ -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&autopause=0&player_id=0&app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} **title**="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:
|
||||
|
||||

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

|
||||
|
||||
```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.
|
||||
:::
|
||||
+174
@@ -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.
|
||||
|
||||

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

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

|
||||
|
||||
<!-- 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&autopause=0&player_id=0&app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="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 -->
|
||||
+55
@@ -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&autopause=0&player_id=0&app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="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 -->
|
||||
+65
@@ -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&autopause=0&player_id=0&app_id=58479" frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen style= {{ position:"absolute",top:0,left:0,width:"100%",height:"100%"}} title="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');
|
||||
},
|
||||
};
|
||||
```
|
||||
+30
@@ -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.
|
||||
+65
@@ -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
|
||||
|
||||

|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[wikipedia]: https://en.wikipedia.org/wiki/White-label_product
|
||||
<!-- prettier-ignore-end -->
|
||||
Reference in new issue
Block a user