Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add mode support and customizeable modes trying to get to the ability to customize modes #4877

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
839 changes: 387 additions & 452 deletions bun.lock

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions extensions/cornerstone/src/getCustomizationModule.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import miscCustomization from './customizations/miscCustomization';
import captureViewportModalCustomization from './customizations/captureViewportModalCustomization';
import viewportDownloadWarningCustomization from './customizations/viewportDownloadWarningCustomization';
import viewportActionMenuCustomizations from './customizations/viewportActionMenuCustomizations';
import modeLongitudinalCustomizations from './customizations/modeLongitudinalCustomizations';

function getCustomizationModule({ commandsManager, servicesManager }) {
return [
@@ -30,6 +31,7 @@ function getCustomizationModule({ commandsManager, servicesManager }) {
...captureViewportModalCustomization,
...viewportDownloadWarningCustomization,
...viewportActionMenuCustomizations,
...modeLongitudinalCustomizations,
},
},
];
3 changes: 2 additions & 1 deletion modes/longitudinal/package.json
Original file line number Diff line number Diff line change
@@ -46,7 +46,8 @@
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3"
"i18next": "^17.0.3",
"@ohif/mode-support": "3.10.0-beta.133"
},
"devDependencies": {
"webpack": "5.94.0",
161 changes: 34 additions & 127 deletions modes/longitudinal/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,77 +1,33 @@
import i18n from 'i18next';
import { id } from './id';
import initToolGroups from './initToolGroups';
import toolbarButtons from './toolbarButtons';

// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
const NON_IMAGE_MODALITIES = ['ECG', 'SEG', 'RTSTRUCT', 'RTPLAN', 'PR'];

const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
wsiSopClassHandler:
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
};

const cornerstone = {
measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
};

const tracked = {
measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList',
viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked',
};

const dicomsr = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr',
sopClassHandler3D: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d',
viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr',
};

const dicomvideo = {
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
};

const dicompdf = {
sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf',
viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf',
};

const dicomSeg = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
};

const dicomPmap = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap',
viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap',
};

const dicomRT = {
viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt',
sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt',
};
import {
isValidMode,
ohif,
cornerstone,
tracked,
dicomsr,
dicomvideo,
dicompdf,
dicomSeg,
dicomPmap,
dicomRT,
extensionDependenciesLongitudinal as extensionDependencies,
} from '@ohif/mode-support';

function modeFactory({ modeConfiguration, servicesManager }) {
let _activatePanelTriggersSubscriptions = [];
const mode = this;

const extensionDependencies = {
// Can derive the versions at least process.env.from npm_package_version
'@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',
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
'@ohif/extension-cornerstone-dicom-pmap': '^3.0.0',
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
'@ohif/extension-dicom-pdf': '^3.0.1',
'@ohif/extension-dicom-video': '^3.0.1',
};
const {
services: { customizationService },
} = servicesManager;
const { baseCustomizationName = 'mode.longitudinal' } = mode;
const getCustomization = name =>
customizationService.getCustomization(`${baseCustomizationName}.${name}`) ||
customizationService.getCustomization(`mode.default.${name}`);

function modeFactory({ modeConfiguration }) {
let _activatePanelTriggersSubscriptions = [];
return {
// TODO: We're using this as a route segment
// We should not be.
@@ -83,57 +39,15 @@ function modeFactory({ modeConfiguration }) {
*/
onModeEnter: function ({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
const { measurementService, toolbarService, toolGroupService } = servicesManager.services;

measurementService.clearMeasurements();

// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);

toolbarService.addButtons(toolbarButtons);
toolbarService.createButtonSection('primary', [
'MeasurementTools',
'Zoom',
'Pan',
'TrackballRotate',
'WindowLevel',
'Capture',
'Layout',
'Crosshairs',
'MoreTools',
]);

toolbarService.createButtonSection('measurementSection', [
'Length',
'Bidirectional',
'ArrowAnnotate',
'EllipticalROI',
'RectangleROI',
'CircleROI',
'PlanarFreehandROI',
'SplineROI',
'LivewireContour',
]);
mode.initToolGroups(extensionManager, toolGroupService, commandsManager);

toolbarService.createButtonSection('moreToolsSection', [
'Reset',
'rotate-right',
'flipHorizontal',
'ImageSliceSync',
'ReferenceLines',
'ImageOverlayViewer',
'StackScroll',
'invert',
'Probe',
'Cine',
'Angle',
'CobbAngle',
'Magnify',
'CalibrationLine',
'TagBrowser',
'AdvancedMagnify',
'UltrasoundDirectionalTool',
'WindowLevelRegion',
]);
toolbarService.addButtons(getCustomization('toolbarButtons'));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pedrokohler - to customize the toolbar, you will also need these two lines, and to create a new custom section that extends the default tools/buttons.

for (const [key, value] of Object.entries(getCustomization('toolbarSections'))) {
toolbarService.createButtonSection(key, value);
}

// // ActivatePanel event trigger for when a segmentation or measurement is added.
// // Do not force activation so as to respect the state the user may have left the UI in.
@@ -189,17 +103,7 @@ function modeFactory({ modeConfiguration }) {
series: [],
},

isValidMode: function ({ modalities }) {
const modalities_list = modalities.split('\\');

// Exclude non-image modalities
return {
valid: !!modalities_list.filter(modality => NON_IMAGE_MODALITIES.indexOf(modality) === -1)
.length,
description:
'The mode does not support studies that ONLY include the following modalities: SM, ECG, SEG, RTSTRUCT',
};
},
isValidMode,
routes: [
{
path: 'longitudinal',
@@ -254,6 +158,7 @@ function modeFactory({ modeConfiguration }) {
extensions: extensionDependencies,
// Default protocol gets self-registered by default in the init
hangingProtocol: 'default',

// Order is important in sop class handlers when two handlers both use
// the same sop class under different situations. In that case, the more
// general handler needs to come last. For this case, the dicomvideo must
@@ -277,7 +182,9 @@ const mode = {
id,
modeFactory,
extensionDependencies,
initToolGroups,
baseCustomizationName: 'mode.longitudinal',
};

export default mode;
export { initToolGroups, toolbarButtons };
export { initToolGroups };
547 changes: 0 additions & 547 deletions modes/longitudinal/src/toolbarButtons.ts

This file was deleted.

12 changes: 12 additions & 0 deletions modes/mode-support/.webpack/webpack.dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.base.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');

const ENTRY = {
app: `${SRC_DIR}/index.ts`,
};

module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
};
53 changes: 53 additions & 0 deletions modes/mode-support/.webpack/webpack.prod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const pkg = require('./../package.json');
const webpackCommon = require('./../../../.webpack/webpack.base.js');

const ROOT_DIR = path.join(__dirname, './../');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
const ENTRY = {
app: `${SRC_DIR}/index.ts`,
};

module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });

return merge(commonConfig, {
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: false,
},
output: {
path: ROOT_DIR,
library: 'ohif-mode-longitudinal',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
// new MiniCssExtractPlugin({
// filename: './dist/[name].css',
// chunkFilename: './dist/[id].css',
// }),
],
});
};
4 changes: 4 additions & 0 deletions modes/mode-support/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
21 changes: 21 additions & 0 deletions modes/mode-support/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Open Health Imaging Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
60 changes: 60 additions & 0 deletions modes/mode-support/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Measurement Tracking Mode



## Introduction
Measurement tracking mode allows you to:

- Draw annotations and have them shown in the measurement panel
- Create a report from the tracked measurement and export them as DICOM SR
- Use already exported DICOM SR to re-hydrate the measurements in the viewer

![preview](https://user-images.githubusercontent.com/7490180/171255703-e6d46da8-8d12-4685-b358-0c8d4d5cb5fe.png)

## Workflow


### Status Icon
Each viewport has a left icon indicating whether the series within the viewport contains:

- tracked measurement OR
- untracked measurement OR
- Structured Report OR
- Locked (uneditable) Structured Report

In the following, we will discuss each category.

![tracked](https://user-images.githubusercontent.com/7490180/171255750-c6903338-c295-4553-b8aa-8cb6a8d63943.png)

### Tracked vs Untracked Measurements

OHIF-v3 implements a workflow for measurement tracking that can be seen below.
In summary, when you create an annotation, a prompt will be shown whether to start tracking or not. If you start the tracking, the annotation style will change to a solid line, and annotation details get displayed on the measurement panel. On the other hand, if you decline the tracking prompt, the measurement will be considered "temporary," and annotation style remains as a dashed line and not shown on the right panel, and cannot be exported.

Below, you can see different icons that appear for a tracked vs. untracked series in OHIF-v3.


![workflow](https://user-images.githubusercontent.com/7490180/171255780-dd249cbf-dd61-4e02-8d46-b91e01d53529.png)


### Reading and Writing DICOM SR
OHIF-v3 provides full support for reading, writing and mapping the DICOM Structured Report (SR) to interactable Cornerstone Tools. When you load an already exported DICOM SR into the viewer, you will be prompted whether to track the measurements for the series or not.


![preview](https://user-images.githubusercontent.com/7490180/171255797-6c374780-8e94-4a7f-a125-69b67c18c18c.png)

If you click Yes, DICOM SR measurements gets re-hydrated into the viewer and the series become a tracked series. However, If you say no and later decide to say track the measurements, you can always click on the SR button that will prompt you with the same message again.


![restore](https://user-images.githubusercontent.com/7490180/171255813-8d460bd7-e64d-4bce-9467-ad5cf2615c56.png)

The full workflow for saving measurements to SR and loading SR into the viewer is shown below.

![sr-import](https://user-images.githubusercontent.com/7490180/171255826-c308ead6-9dad-4e91-9411-df62658cc839.png)


### Loading DICOM SR into an Already Tracked Series

If you have an already tracked series and try to load a DICOM SR measurements, you will be shown the following lock icon. This means that, you can review the DICOM SR measurement, manipulate image and draw "temporary" measurements; however, you cannot edit the DICOM SR measurement.

![locked](https://user-images.githubusercontent.com/7490180/171255842-91b84f91-4e1c-4a20-b4a2-cf9653560c43.png)
1 change: 1 addition & 0 deletions modes/mode-support/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../babel.config.js');
45 changes: 45 additions & 0 deletions modes/mode-support/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@ohif/mode-support",
"version": "3.10.0-beta.133",
"description": "Support Class for Modes",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/ohif-mode-longitudinal.js",
"module": "src/index.ts",
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"files": [
"dist",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"clean": "shx rm -rf dist",
"clean:deep": "yarn run clean && shx rm -rf node_modules",
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/core": "3.10.0-beta.133",
"@ohif/extension-default": "3.10.0-beta.133"
},
"dependencies": {
"@babel/runtime": "^7.20.13",
"i18next": "^17.0.3"
},
"devDependencies": {
"webpack": "5.94.0",
"webpack-merge": "^5.7.3"
}
}
81 changes: 81 additions & 0 deletions modes/mode-support/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Allow this mode by excluding non-imaging modalities such as SR, SEG
// Also, SM is not a simple imaging modalities, so exclude it.
export const NON_IMAGE_MODALITIES = ['ECG', 'SEG', 'RTSTRUCT', 'RTPLAN', 'PR'];

export const ohif = {
layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout',
sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack',
thumbnailList: '@ohif/extension-default.panelModule.seriesList',
wsiSopClassHandler:
'@ohif/extension-cornerstone.sopClassHandlerModule.DicomMicroscopySopClassHandler',
};

export const cornerstone = {
measurements: '@ohif/extension-cornerstone.panelModule.panelMeasurement',
segmentation: '@ohif/extension-cornerstone.panelModule.panelSegmentation',
};

export const tracked = {
measurements: '@ohif/extension-measurement-tracking.panelModule.trackedMeasurements',
thumbnailList: '@ohif/extension-measurement-tracking.panelModule.seriesList',
viewport: '@ohif/extension-measurement-tracking.viewportModule.cornerstone-tracked',
};

export const dicomsr = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr',
sopClassHandler3D: '@ohif/extension-cornerstone-dicom-sr.sopClassHandlerModule.dicom-sr-3d',
viewport: '@ohif/extension-cornerstone-dicom-sr.viewportModule.dicom-sr',
};

export const dicomvideo = {
sopClassHandler: '@ohif/extension-dicom-video.sopClassHandlerModule.dicom-video',
viewport: '@ohif/extension-dicom-video.viewportModule.dicom-video',
};

export const dicompdf = {
sopClassHandler: '@ohif/extension-dicom-pdf.sopClassHandlerModule.dicom-pdf',
viewport: '@ohif/extension-dicom-pdf.viewportModule.dicom-pdf',
};

export const dicomSeg = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
viewport: '@ohif/extension-cornerstone-dicom-seg.viewportModule.dicom-seg',
};

export const dicomPmap = {
sopClassHandler: '@ohif/extension-cornerstone-dicom-pmap.sopClassHandlerModule.dicom-pmap',
viewport: '@ohif/extension-cornerstone-dicom-pmap.viewportModule.dicom-pmap',
};

export const dicomRT = {
viewport: '@ohif/extension-cornerstone-dicom-rt.viewportModule.dicom-rt',
sopClassHandler: '@ohif/extension-cornerstone-dicom-rt.sopClassHandlerModule.dicom-rt',
};

export const extensionDependenciesBasic = {
// Can derive the versions at least process.env.from npm_package_version
'@ohif/extension-default': '^3.0.0',
'@ohif/extension-cornerstone': '^3.0.0',
'@ohif/extension-cornerstone-dicom-sr': '^3.0.0',
'@ohif/extension-cornerstone-dicom-seg': '^3.0.0',
'@ohif/extension-cornerstone-dicom-pmap': '^3.0.0',
'@ohif/extension-cornerstone-dicom-rt': '^3.0.0',
'@ohif/extension-dicom-pdf': '^3.0.1',
'@ohif/extension-dicom-video': '^3.0.1',
};

export const extensionDependenciesLongitudinal = {
...extensionDependenciesBasic,
'@ohif/extension-measurement-tracking': '^3.0.0',
};

export function isValidMode({ modalities }) {
const nonImageModalities = this.nonImageModalities || NON_IMAGE_MODALITIES;
const modalities_list = modalities.split('\\');

// Exclude non-image modalities
return {
valid: !!modalities_list.filter(modality => nonImageModalities.indexOf(modality) === -1).length,
description: `The mode does not support studies that ONLY include the following modalities: ${nonImageModalities.join(', ')}`,
};
}
7 changes: 6 additions & 1 deletion platform/app/src/appInit.js
Original file line number Diff line number Diff line change
@@ -117,7 +117,12 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
? appConfig.modesConfiguration[id]
: {};

mode = await mode.modeFactory({ modeConfiguration, loadModules });
mode = await mode.modeFactory({
modeConfiguration,
loadModules,
servicesManager,
extensionManager,
});
}

if (modesById.has(id)) {