feat(customizableOverlay): Add customizable overlay info (#3061)
* overlay customization * custom overlay definition and examples * minor fix - import statement difference between cornerstone and cornerstone3D * move "VOI" and "Zoom" calulation to the ViewportOverlay component, make OverlayItem fairly dull * type specifications * follow up fixes for CustomizationService changes on upstream * [fix] various fixes related to custom overlays (formatting, toggling) * [fix] various fixes related to custom overlays (formatting, toggling) * right side overlay panels - align right * [fix] remove "notification" from toggleOverlay command * comments * [refactor] remove unused imports * documentation of customizable overlay configuration * prettify * review of documentation of customization service * fix useCallback() dependencies * fix useCallback dependencies * documentation for customization service * documentation for customization service
This commit is contained in:
1 parent
b9dbc5b3a5
commit
e4e62e9e14
10 files changed
+992
-120
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -17,135 +17,149 @@ This service is a UI service in that part of the registration allows for registe
|
||||
UI components and types to deal with, but it does not directly provide an UI
|
||||
displayable elements unless customized to do so.
|
||||
|
||||
## Registering Customizations
|
||||
<b>Note:</b> Customization Service 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. (for example, `CustomizableViewportOverlay` component uses
|
||||
`CustomizationService` to implement viewport overlay that is easily customizable
|
||||
from configuration.)
|
||||
|
||||
## Registering customizable modules (or defining customization prototypes)
|
||||
|
||||
Extensions and Modes can register customization templates they support.
|
||||
It is done by adding `getCustomizationModule()` in the extension or mode definition.
|
||||
|
||||
Below is the protocol of the `getCustomizationModule()`, if defined in Typescript.
|
||||
|
||||
```typescript
|
||||
getCustomizationModule() : { name: string, value: any }[]
|
||||
```
|
||||
|
||||
If the name is 'default', it is the Default customization, which is loaded
|
||||
automatically when the extension or mode is loaded.
|
||||
|
||||
In the `value` of each customizations, you will define customization prototype(s).
|
||||
These customization prototype(s) can be considered like "Prototype" in Javascript.
|
||||
These can be used to extend the customization definitions from configurations.
|
||||
Default cutomizations will be often used to define all the customization prototypes,
|
||||
as they will be loaded automatically along with the defining extension or mode.
|
||||
|
||||
|
||||
For example, the `@ohif/extension-default` extension defines,
|
||||
|
||||
```js
|
||||
getCustomizationModule: () => [
|
||||
//...
|
||||
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
{
|
||||
id: 'ohif.overlayItem',
|
||||
uiType: 'uiType',
|
||||
content: function (props) {
|
||||
if (this.condition && !this.condition(props)) return null;
|
||||
|
||||
const { instance } = props;
|
||||
const value =
|
||||
instance && this.attribute
|
||||
? instance[this.attribute]
|
||||
: this.contentF && typeof this.contentF === 'function'
|
||||
? this.contentF(props)
|
||||
: null;
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="overlay-item flex flex-row"
|
||||
style={{ color: this.color || undefined }}
|
||||
title={this.title || ''}
|
||||
>
|
||||
{this.label && (
|
||||
<span className="mr-1 shrink-0">{this.label}</span>
|
||||
)}
|
||||
<span className="font-light">{value}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
//...
|
||||
],
|
||||
```
|
||||
|
||||
And this `ohif.overlayItem` object will be used as a prototype to define items
|
||||
to be displayed on `CustomizableViewportOverlay`. See the next section.
|
||||
|
||||
## Configuring customizations
|
||||
|
||||
There are several ways to register customizations. The
|
||||
`APP_CONFIG.customizationService`
|
||||
field is used as a per-configuration entry. This object can list single
|
||||
configurations by id, or it can list sets of customizations by referring to
|
||||
the `customizationModule` in an extension. For example, the fictitious
|
||||
customization 'customIcons' might be defined as below in the APP_CONFIG:
|
||||
the `customizationModule` in an extension.
|
||||
|
||||
NOTE that these definitions from APP_CONFIG will be loaded by default, just like
|
||||
extension/modes default customization.
|
||||
|
||||
Below is the example configuration for `CustomizableViewportOverlay` component
|
||||
customization, using the customization prototype `ohif.overlayItem` defined in
|
||||
`ohif/extension-defaul` extension.:
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
...,
|
||||
customizationService: [
|
||||
{
|
||||
id: 'customIcons',
|
||||
backArrow: 'https://customIcons.org/backArrow.svg',
|
||||
//...
|
||||
|
||||
// in the APP_CONFIG file set the top right area to show the patient name
|
||||
// using PN: as a prefix when the study has a non-empty patient name.
|
||||
customizationService: {
|
||||
cornerstoneOverlayTopRight: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
customizationType: 'ohif.cornerstoneOverlay',
|
||||
items: [
|
||||
{
|
||||
id: 'PatientNameOverlay',
|
||||
// Note the overlayItem as a parent type - this provides the
|
||||
// rendering functionality to read the attribute and use the label.
|
||||
customizationType: 'ohif.overlayItem',
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
title: 'Patient Name',
|
||||
color: 'yellow',
|
||||
condition: ({ instance }) =>
|
||||
instance &&
|
||||
instance.PatientName &&
|
||||
instance.PatientName.Alphabetic,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName.Alphabetic) +
|
||||
' ' +
|
||||
(instance.PatientSex ? '(' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
...
|
||||
},
|
||||
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
As well, extensions can register default customizations by providing a 'default'
|
||||
name key within the extension. These are simply customizations loaded when
|
||||
the extension is loaded. For example, the previous customization could have
|
||||
been added in an extension as:
|
||||
In the customization configuration, you can use `customizationType` fields to
|
||||
define the prototype that customization object should inherit from.
|
||||
The `customizationType` field is simply the id of another customization object.
|
||||
|
||||
```js
|
||||
getCustomizationModule: () => [
|
||||
{
|
||||
name: 'default',
|
||||
value: {
|
||||
id: 'customIcons',
|
||||
backArrow: 'https://customIcons.org/backArrow.svg',
|
||||
},
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
Note the name of this is default (thus loaded automatically instead of by
|
||||
reference), and the value is a customization of customIcons.
|
||||
## Implementing customization using CustomizationService
|
||||
|
||||
The type and parameters of a customization are defined by the user of the
|
||||
customization, based on the customization id. For example, `cornerstoneOverlay`
|
||||
is a customization that is a React component, so it requires a react content,
|
||||
and optionally contentProps which are used to supply values to the content.
|
||||
|
||||
The extension can also supply a default parent instance to inherit values from.
|
||||
This allows the content or other parameters to be pre-filled, and only the
|
||||
required values changed. The parent to use is specified by the `customizationType` field,
|
||||
and is simply the id of another customization object. An example of this might
|
||||
be a demographics overlay field, where the base version needs an actual component,
|
||||
while the typed version just needs the attribute and label to use.
|
||||
|
||||
```js
|
||||
getCustomizationModule: () => [
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
// This first value defines the base type
|
||||
{
|
||||
id: imageDemographicOverlay,
|
||||
content: function({image}) {
|
||||
return (<p>{image[this.attribute]}</p>);
|
||||
}
|
||||
},
|
||||
// The second one defines an instance.
|
||||
// It may or may not use the previous type definition - it will use it
|
||||
// if nothing replaces the previous definition, otherwise it will use the new one.
|
||||
{
|
||||
id: PatientIDOverlayItem,
|
||||
customizationType: 'imageDemographicOverlay',
|
||||
attribute: 'PatientID',
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
### Mode Customizations
|
||||
|
||||
Mode-specific customizations are no different from the global ones,
|
||||
except that the mode customizations are cleared before the mode `onModeEnter`
|
||||
is called, and they can have new values registered in the `onModeEnter`
|
||||
|
||||
The following example shows first the registration of the default instances,
|
||||
and then shows how they might be used.
|
||||
|
||||
```js
|
||||
// In the cornerstone extension getCustomizationModule:
|
||||
const getCustomizationModule = () => ([
|
||||
{
|
||||
name: 'default',
|
||||
value: [
|
||||
{
|
||||
id: 'ohif.cornerstoneOverlay',
|
||||
content: CornerstoneOverlay,
|
||||
// Requires items on instances
|
||||
},
|
||||
{
|
||||
id: 'ohif.overlayItem',
|
||||
content: CornerstoneOverlayItem,
|
||||
// Requires attribute and label on instances
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
```
|
||||
|
||||
Then, in the configuration file one might have a custom overlay definition:
|
||||
|
||||
```js
|
||||
// in the APP_CONFIG file set the top right area to show the patient name
|
||||
// using PN: as a prefix when the study has a non-empty patient name.
|
||||
customizationService: {
|
||||
cornerstoneOverlayTopRight: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
customizationType: 'ohif.cornerstoneOverlay',
|
||||
items: [
|
||||
{
|
||||
id: 'PatientNameOverlay',
|
||||
// Note the ohif.overlayItem is a prototype instance for this object
|
||||
// The ohif.overlayItem is defined up above
|
||||
customizationType: 'ohif.overlayItem',
|
||||
attribute: 'PatientName',
|
||||
label: 'PN:',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
In the mode customization, the overlay is then further customized
|
||||
with a bottom-right overlay, which extends the customizationService configuration.
|
||||
|
||||
@@ -170,7 +184,6 @@ onModeEnter() {
|
||||
customizationService.addModeCustomizations(bottomRight);
|
||||
```
|
||||
|
||||
## Mode Customizations
|
||||
The mode customizations are retrieved via the `getModeCustomization` function,
|
||||
providing an id, and optionally a default value. The retrieval will return,
|
||||
in order:
|
||||
@@ -201,11 +214,13 @@ uses commands lists):
|
||||
uiConfigurationService.recordInteraction(cornerstoneContextMenu, extraProps);
|
||||
```
|
||||
|
||||
## Global Customizations
|
||||
### Global Customizations
|
||||
|
||||
Global customizations are retrieved in the same was as mode customizations, except
|
||||
that the `getGlobalCustomization` is called instead of the mode call.
|
||||
|
||||
## Types
|
||||
### Types
|
||||
|
||||
Some types for the customization service are provided by the `@ohif/ui` types
|
||||
export. Additionally, extensions can provide a Types export with custom
|
||||
typing, allowing for better typing for the extension specific capabilities.
|
||||
@@ -223,7 +238,8 @@ const customContextMenu: Types.UIContextMenu =
|
||||
},
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
### Inheritance
|
||||
|
||||
JavaScript property inheritance can be supplied by defining customizations
|
||||
with id corresponding to the customizationType value. For example:
|
||||
|
||||
@@ -256,10 +272,12 @@ const overlayItem: Types.UIOverlayItem = {
|
||||
```
|
||||
|
||||
# Customizations
|
||||
|
||||
This section can be used to specify various customization capabilities.
|
||||
|
||||
|
||||
## Text color for StudyBrowser tabs
|
||||
|
||||
This is the recommended pattern for deep customization of class attributes,
|
||||
making it fine grained, and have it apply a set of attributes, mostly from
|
||||
tailwind. In this case it is a double indirection, as the buttons class
|
||||
@@ -310,6 +328,156 @@ customizationService: [
|
||||
],
|
||||
```
|
||||
|
||||
## Customizable Viewport Overlay
|
||||
|
||||
Below is the full example configuration of the customizable viewport overlay and the screenshot of the result overlay.
|
||||
|
||||
```javascript
|
||||
// this is one of the configuration files in `platform/viewer/public/config/*.js`
|
||||
window.config = {
|
||||
// ...
|
||||
|
||||
customizationService: {
|
||||
cornerstoneOverlayTopLeft: {
|
||||
id: 'cornerstoneOverlayTopLeft',
|
||||
customizationType: 'ohif.cornerstoneOverlay',
|
||||
items: [
|
||||
{
|
||||
id: 'WindowLevel',
|
||||
customizationType: 'ohif.overlayItem.windowLevel',
|
||||
},
|
||||
{
|
||||
id: 'PatientName',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
color: 'green',
|
||||
background: 'white',
|
||||
condition: ({ instance }) =>
|
||||
instance && instance.PatientName && instance.PatientName.Alphabetic,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.PatientName.Alphabetic) +
|
||||
' ' +
|
||||
(instance.PatientSex ? '(' + instance.PatientSex + ')' : ''),
|
||||
},
|
||||
{
|
||||
id: 'Species',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Species:',
|
||||
condition: ({ instance }) =>
|
||||
instance && instance.PatientSpeciesDescription,
|
||||
contentF: ({ instance }) =>
|
||||
instance.PatientSpeciesDescription +
|
||||
'/' +
|
||||
instance.PatientBreedDescription,
|
||||
},
|
||||
{
|
||||
id: 'PID',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'PID:',
|
||||
title: 'Patient PID',
|
||||
condition: ({ instance }) => instance && instance.PatientID,
|
||||
contentF: ({ instance }) => instance.PatientID,
|
||||
},
|
||||
{
|
||||
id: 'PatientBirthDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'DOB:',
|
||||
title: "Patient's Date of birth",
|
||||
condition: ({ instance }) => instance && instance.PatientBirthDate,
|
||||
contentF: ({ instance }) => instance.PatientBirthDate,
|
||||
},
|
||||
{
|
||||
id: 'OtherPid',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Other PID:',
|
||||
title: 'Other Patient IDs',
|
||||
condition: ({ instance }) => instance && instance.OtherPatientIDs,
|
||||
contentF: ({ instance, formatters: { formatPN } }) =>
|
||||
formatPN(instance.OtherPatientIDs),
|
||||
},
|
||||
],
|
||||
},
|
||||
cornerstoneOverlayTopRight: {
|
||||
id: 'cornerstoneOverlayTopRight',
|
||||
customizationType: 'ohif.cornerstoneOverlay',
|
||||
|
||||
items: [
|
||||
{
|
||||
id: 'InstanceNmber',
|
||||
customizationType: 'ohif.overlayItem.instanceNumber',
|
||||
},
|
||||
{
|
||||
id: 'StudyDescription',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: ({ instance }) =>
|
||||
instance &&
|
||||
instance.StudyDescription &&
|
||||
`Study Description: ${instance.StudyDescription}`,
|
||||
condition: ({ instance }) => instance && instance.StudyDescription,
|
||||
contentF: ({ instance }) => instance.StudyDescription,
|
||||
},
|
||||
{
|
||||
id: 'StudyDate',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study date',
|
||||
condition: ({ instance }) => instance && instance.StudyDate,
|
||||
contentF: ({ instance, formatters: { formatDate } }) =>
|
||||
formatDate(instance.StudyDate),
|
||||
},
|
||||
{
|
||||
id: 'StudyTime',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: '',
|
||||
title: 'Study time',
|
||||
condition: ({ instance }) => instance && instance.StudyTime,
|
||||
contentF: ({ instance, formatters: { formatTime } }) =>
|
||||
formatTime(instance.StudyTime),
|
||||
},
|
||||
],
|
||||
},
|
||||
cornerstoneOverlayBottomLeft: {
|
||||
id: 'cornerstoneOverlayBottomLeft',
|
||||
customizationType: 'ohif.cornerstoneOverlay',
|
||||
|
||||
items: [
|
||||
{
|
||||
id: 'SeriesNumber',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Ser:',
|
||||
title: 'Series Number',
|
||||
condition: ({ instance }) => instance && instance.SeriesNumber,
|
||||
contentF: ({ instance }) => instance.SeriesNumber,
|
||||
},
|
||||
{
|
||||
id: 'SliceLocation',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Loc:',
|
||||
title: 'Slice Location',
|
||||
condition: ({ instance }) => instance && instance.SliceLocation,
|
||||
contentF: ({ instance, formatters: { formatNumberPrecision } }) =>
|
||||
formatNumberPrecision(instance.SliceLocation, 2) + ' mm',
|
||||
},
|
||||
{
|
||||
id: 'SliceThickness',
|
||||
customizationType: 'ohif.overlayItem',
|
||||
label: 'Thick:',
|
||||
title: 'Slice Thickness',
|
||||
condition: ({ instance }) => instance && instance.SliceThickness,
|
||||
contentF: ({ instance, formatters: { formatNumberPrecision } }) =>
|
||||
formatNumberPrecision(instance.SliceThickness, 2) + ' mm',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
<img src="../../../assets/img/customizable-overlay.png" />
|
||||
|
||||
> 3rd Party implementers may be added to this table via pull requests.
|
||||
|
||||
<!--
|
||||
|
||||
Reference in new issue
Block a user