diff --git a/extensions/dicom-sr/.webpack/webpack.dev.js b/extensions/dicom-sr/.webpack/webpack.dev.js new file mode 100644 index 000000000..1ae308448 --- /dev/null +++ b/extensions/dicom-sr/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/extensions/dicom-sr/.webpack/webpack.prod.js b/extensions/dicom-sr/.webpack/webpack.prod.js new file mode 100644 index 000000000..ca612cd37 --- /dev/null +++ b/extensions/dicom-sr/.webpack/webpack.prod.js @@ -0,0 +1,44 @@ +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); + + return merge(commonConfig, { + devtool: 'source-map', + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'OHIFExtCornerstone', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + }); +}; diff --git a/extensions/dicom-sr/LICENSE b/extensions/dicom-sr/LICENSE new file mode 100644 index 000000000..19e20dd35 --- /dev/null +++ b/extensions/dicom-sr/LICENSE @@ -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. diff --git a/extensions/dicom-sr/README.md b/extensions/dicom-sr/README.md new file mode 100644 index 000000000..9c6e86015 --- /dev/null +++ b/extensions/dicom-sr/README.md @@ -0,0 +1,121 @@ +# @ohif/extension-cornerstone + +![npm (scoped)](https://img.shields.io/npm/v/@ohif/extension-cornerstone.svg?style=flat-square) + +This extension adds support for viewing and manipulating 2D medical images via a +viewport. The underlying implementation wraps the +`cornerstonejs/react-cornerstone-viewport`, and provides basic commands and +toolbar buttons for common actions. + + + +#### Index + +Extension Id: `cornerstone` + +- [Commands Module](#commands-module) +- [Toolbar Module](#toolbar-module) +- [Viewport Module](#viewport-module) + +## Commands Module + +This extensions includes the following `Commands` and `Command Definitions`. +These can be registered with `@ohif/core`'s `CommandManager`. After registering +the commands, they can be bound to `hotkeys` using the `HotkeysManager` and +listed in the `UserPreferences` modal. + +You can read more about [`Commands`][docs-commands], [`Hotkeys`][docs-hotkeys], +and the [`UserPreferences` Modal][docs-userprefs] in their respective locations +in the OHIF Viewer's documentation. + +| Command Name | Description | Store Contexts | +| ---------------------------- | --------------------------------------- | -------------- | +| `rotateViewportCW` | | viewports | +| `rotateViewportCCW` | | viewports | +| `invertViewport` | | viewports | +| `flipViewportVertical` | | viewports | +| `flipViewportHorizontal` | | viewports | +| `scaleUpViewport` | | viewports | +| `scaleDownViewport` | | viewports | +| `fitViewportToWindow` | | viewports | +| `resetViewport` | | viewports | +| clearAnnotations | TODO | | +| next/previous Image | TODO | | +| first/last Image | TODO | | +| `nextViewportDisplaySet` | | | +| `previousViewportDisplaySet` | | | +| `setToolActive` | Activates tool for primary button/touch | | + +## Toolbar Module + +Our toolbar module contains definitions for: + +- `StackScroll` +- `Zoom` +- `Wwwc` +- `Pan` +- `Length` +- `Angle` +- `Reset` +- `Cine` + +All use the `ACTIVE_VIEWPORT::CORNERSTONE` context. + +## Viewport Module + +Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport] +and is connected the redux store. This module is the most prone to change as we +hammer out our Viewport interface. + +## Tool Configuration + +Tools can be configured through extension configuration using the tools key: + +```js + ... + cornerstoneExtensionConfig: { + tools: { + ArrowAnnotate: { + configuration: { + getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')), + }, + }, + }, + }, + ... +``` + +## Annotate Tools Configuration + +*We currently support one property for annotation tools.* + +### Hide handles +This extension configuration allows you to toggle on/off handle rendering for all annotate tools: + +```js + ... + cornerstoneExtensionConfig: { + hideHandles: true, + }, + ... + +## Resources + +### Repositories + +- [cornerstonejs/react-cornerstone-viewport][react-viewport] +- [cornerstonejs/cornerstoneTools][cornerstone-tools] +- [cornerstonejs/cornerstone][cornerstone] + + + + +[docs-commands]: https://www.com +[docs-hotkeys]: https://www.com +[docs-userprefs]: htt +[react-viewport]: https://github.com/cornerstonejs/react-cornerstone-viewport +[cornerstone-tools]: https://github.com/cornerstonejs/cornerstoneTools +[cornerstone]: https://github.com/cornerstonejs/cornerstone + diff --git a/extensions/dicom-sr/babel.config.js b/extensions/dicom-sr/babel.config.js new file mode 100644 index 000000000..325ca2a8e --- /dev/null +++ b/extensions/dicom-sr/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json new file mode 100644 index 000000000..6b14984c9 --- /dev/null +++ b/extensions/dicom-sr/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ohif/extension-dicom-sr", + "version": "0.0.1", + "description": "OHIF extension for an SR Cornerstone Viewport", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=10", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --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" + }, + "peerDependencies": { + "@ohif/core": "^0.50.0", + "@ohif/ui": "^0.50.0", + "cornerstone-core": "^2.3.0", + "cornerstone-math": "^0.1.8", + "cornerstone-tools": "4.15.1", + "dcmjs": "^0.12.3", + "cornerstone-wado-image-loader": "^3.1.2", + "dicom-parser": "^1.8.3", + "hammerjs": "^2.0.8", + "prop-types": "^15.6.2", + "react": "^16.11.0", + "react-dom": "^16.11.0", + "react-redux": "^6.0.0", + "react-resize-detector": "^3.4.0", + "redux": "^4.0.1" + }, + "dependencies": { + "@babel/runtime": "7.7.6", + "classnames": "^2.2.6", + "lodash.merge": "^4.6.2", + "lodash.throttle": "^4.1.1", + "react-cornerstone-viewport": "2.3.8" + } +} diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js new file mode 100644 index 000000000..ad1cf72f9 --- /dev/null +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -0,0 +1,232 @@ +import React, { Component } from 'react'; +import CornerstoneViewport from 'react-cornerstone-viewport'; +//import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport'; +import OHIF from '@ohif/core'; +import PropTypes from 'prop-types'; +import cornerstone from 'cornerstone-core'; +import debounce from 'lodash.debounce'; +import throttle from 'lodash.throttle'; + +// const { +// onAdded, +// onRemoved, +// onModified, +// } = OHIF.measurements.MeasurementHandlers; + +// // TODO: Transition to enums for the action names so that we can ensure they stay up to date +// // everywhere they're used. +// const MEASUREMENT_ACTION_MAP = { +// added: onAdded, +// removed: onRemoved, +// modified: throttle(event => { +// return onModified(event); +// }, 300), +// }; + +// const cine = viewportSpecificData.cine; + +// isPlaying = cine.isPlaying === true; +// frameRate = cine.cineFrameRate || frameRate; + +const { StackManager } = OHIF.utils; + +class OHIFCornerstoneViewport extends Component { + state = { + viewportData: null, + }; + + static defaultProps = { + customProps: {}, + }; + + static propTypes = { + displaySet: PropTypes.object, + viewportIndex: PropTypes.number, + dataSource: PropTypes.object, + children: PropTypes.node, + customProps: PropTypes.object, + }; + + static name = 'OHIFCornerstoneViewport'; + + static init() { + console.log('OHIFCornerstoneViewport init()'); + } + + static destroy() { + console.log('OHIFCornerstoneViewport destroy()'); + StackManager.clearStacks(); + } + + /** + * Obtain the CornerstoneTools Stack for the specified display set. + * + * @param {Object} displaySet + * @param {Object} dataSource + * @return {Object} CornerstoneTools Stack + */ + static getCornerstoneStack(displaySet, dataSource) { + const { frameIndex } = displaySet; + + // Get stack from Stack Manager + const storedStack = StackManager.findOrCreateStack(displaySet, dataSource); + + // Clone the stack here so we don't mutate it + const stack = Object.assign({}, storedStack); + + stack.currentImageIdIndex = frameIndex; + + // TODO -> Do we ever use this like this? + // if (SOPInstanceUID) { + // const index = stack.imageIds.findIndex(imageId => { + // const imageIdSOPInstanceUID = cornerstone.metaData.get( + // 'SOPInstanceUID', + // imageId + // ); + + // return imageIdSOPInstanceUID === SOPInstanceUID; + // }); + + // if (index > -1) { + // stack.currentImageIdIndex = index; + // } else { + // console.warn( + // 'SOPInstanceUID provided was not found in specified DisplaySet' + // ); + // } + // } + + return stack; + } + + getViewportData = async displaySet => { + let viewportData; + + const { dataSource } = this.props; + + const stack = OHIFCornerstoneViewport.getCornerstoneStack( + displaySet, + dataSource + ); + + viewportData = { + StudyInstanceUID: displaySet.StudyInstanceUID, + displaySetInstanceUID: displaySet.displaySetInstanceUID, + stack, + }; + + return viewportData; + }; + + setStateFromProps() { + const { displaySet } = this.props; + const { + StudyInstanceUID, + displaySetInstanceUID, + sopClassUids, + } = displaySet; + + if (!StudyInstanceUID || !displaySetInstanceUID) { + return; + } + + if (sopClassUids && sopClassUids.length > 1) { + console.warn( + 'More than one SOPClassUID in the same series is not yet supported.' + ); + } + + this.getViewportData(displaySet).then(viewportData => { + this.setState({ + viewportData, + }); + }); + } + + componentDidMount() { + this.setStateFromProps(); + } + + componentDidUpdate(prevProps) { + const { displaySet } = this.props; + const prevDisplaySet = prevProps.displaySet; + + if ( + displaySet.displaySetInstanceUID !== + prevDisplaySet.displaySetInstanceUID || + displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID || + displaySet.frameIndex !== prevDisplaySet.frameIndex + ) { + this.setStateFromProps(); + } + } + + render() { + let childrenWithProps = null; + + if (!this.state.viewportData) { + return null; + } + const { viewportIndex } = this.props; + const { + imageIds, + currentImageIdIndex, + // If this comes from the instance, would be a better default + // `FrameTime` in the instance + // frameRate = 0, + } = this.state.viewportData.stack; + + // TODO: Does it make more sense to use Context? + if (this.props.children && this.props.children.length) { + childrenWithProps = this.props.children.map((child, index) => { + return ( + child && + React.cloneElement(child, { + viewportIndex: this.props.viewportIndex, + key: index, + }) + ); + }); + } + + const debouncedNewImageHandler = debounce( + ({ currentImageIdIndex, sopInstanceUid }) => { + const { displaySet } = this.props; + const { StudyInstanceUID } = displaySet; + if (currentImageIdIndex > 0) { + this.props.onNewImage({ + StudyInstanceUID, + SOPInstanceUID: sopInstanceUid, + frameIndex: currentImageIdIndex, + activeViewportIndex: viewportIndex, + }); + } + }, + 700 + ); + + // TODO -> We may still want a wrapped component to define all the measurement api stuff. + + return ( + <> + + {childrenWithProps} + + ); + } +} + +const temp = () =>
Hello SR Viewport!
; + +//export default OHIFCornerstoneViewport; +export default temp; diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js new file mode 100644 index 000000000..b4bbcba83 --- /dev/null +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -0,0 +1,146 @@ +import id from './id'; +import { utils } from '@ohif/core'; + +const sopClassHandlerName = 'dicom-sr'; + +const sopClassUids = [ + '1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR: + '1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR: + '1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR: +]; + +const scoordTypes = ['POINT', 'MULTIPOINT', 'POLYLINE', 'CIRCLE', 'ELLIPSE']; + +const CodeNameCodeSequenceValues = { + ImagingMeasurementReport: '126000', + ImageLibrary: '111028', + ImagingMeasurements: '126010', + MeasurementGroup: '125007', + ImageLibraryGroup: '126200', +}; + +/** + * Basic SOPClassHandler: + * - For all Image types that are stackable, create + * a displaySet with a stack of images + * + * @param {Array} sopClassHandlerModules List of SOP Class Modules + * @param {SeriesMetadata} series The series metadata object from which the display sets will be created + * @returns {Array} The list of display sets created for the given series object + */ +function getDisplaySetsFromSeries(instances) { + // If the series has no instances, stop here + if (!instances || !instances.length) { + throw new Error('No instances were provided'); + } + + const instance = instances[0]; + + const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance; + + const { ConceptNameCodeSequence, ContentSequence } = instance; + + if ( + ConceptNameCodeSequence.CodeValue !== + CodeNameCodeSequenceValues.ImagingMeasurementReport + ) { + console.warn( + 'Only support Imaging Measurement Report SRs (TID1500) for now' + ); + return []; + } + + const referencedImages = _getReferencedImagesList(ContentSequence); + + const measurements = _getMeasurements(ContentSequence, SOPInstanceUID); + + const displaySet = { + plugin: id, + Modality: 'SR', + displaySetInstanceUID: utils.guid(), + SOPInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`, + referencedImages, + }; + + return [displaySet]; +} + +function getSopClassHandlerModule() { + return [ + { + name: sopClassHandlerName, + sopClassUids, + getDisplaySetsFromSeries, + }, + ]; +} + +function _getMeasurements( + ImagingMeasurementReportContentSequence, + SOPInstanceUID +) { + const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.ImagingMeasurements + ); + + if (SOPInstanceUID === '2.25.435452399240481307327287169369305113868') { + debugger; + } + + const MeasurementGroup = _getSequenceAsArray( + ImagingMeasurements.ContentSequence + ).find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.MeasurementGroup + ); + + const Measurements = _getSequenceAsArray( + MeasurementGroup.ContentSequence + ).filter(group => group.ValueType === 'NUM'); + + debugger; + + _getSequenceAsArray(MeasurementGroup.ContentSequence).forEach(item => {}); +} + +function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { + const ImageLibrary = ImagingMeasurementReportContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.ImageLibrary + ); + + const ImageLibraryGroup = _getSequenceAsArray( + ImageLibrary.ContentSequence + ).find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.ImageLibraryGroup + ); + + const referencedImages = []; + + _getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => { + const { ReferencedSOPSequence } = item; + const { + ReferencedSOPClassUID, + ReferencedSOPInstanceUID, + } = ReferencedSOPSequence; + + referencedImages.push({ ReferencedSOPClassUID, ReferencedSOPInstanceUID }); + }); + + return referencedImages; +} + +function _getSequenceAsArray(sequence) { + return Array.isArray(sequence) ? sequence : [sequence]; +} + +export default getSopClassHandlerModule; diff --git a/extensions/dicom-sr/src/id.js b/extensions/dicom-sr/src/id.js new file mode 100644 index 000000000..f9ab81f91 --- /dev/null +++ b/extensions/dicom-sr/src/id.js @@ -0,0 +1 @@ +export default 'org.ohif.dicom-sr'; diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js new file mode 100644 index 000000000..533c06381 --- /dev/null +++ b/extensions/dicom-sr/src/index.js @@ -0,0 +1,45 @@ +import React from 'react'; +import getSopClassHandlerModule from './getSopClassHandlerModule'; +import id from './id.js'; + +const Component = React.lazy(() => { + return import('./OHIFCornerstoneSRViewport'); +}); + +const OHIFCornerstoneSRViewport = props => { + return ( + Loading...}> + + + ); +}; + +/** + * + */ +export default { + /** + * Only required property. Should be a unique value across all extensions. + */ + id, + + /** + * + * + * @param {object} [configuration={}] + * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` + */ + getViewportModule({ commandsManager }) { + const ExtendedOHIFCornerstoneSRViewport = props => { + const onNewImageHandler = jumpData => { + commandsManager.runCommand('jumpToImage', jumpData); + }; + return ( + + ); + }; + + return [{ name: 'dicom-sr', component: ExtendedOHIFCornerstoneSRViewport }]; + }, + getSopClassHandlerModule, +}; diff --git a/modes/SRViewportExample/.webpack/webpack.dev.js b/modes/SRViewportExample/.webpack/webpack.dev.js new file mode 100644 index 000000000..1ae308448 --- /dev/null +++ b/modes/SRViewportExample/.webpack/webpack.dev.js @@ -0,0 +1,8 @@ +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + return webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); +}; diff --git a/modes/SRViewportExample/.webpack/webpack.prod.js b/modes/SRViewportExample/.webpack/webpack.prod.js new file mode 100644 index 000000000..ca612cd37 --- /dev/null +++ b/modes/SRViewportExample/.webpack/webpack.prod.js @@ -0,0 +1,44 @@ +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const path = require('path'); +const webpackCommon = require('./../../../.webpack/webpack.commonjs.js'); +const pkg = require('./../package.json'); + +const ROOT_DIR = path.join(__dirname, './..'); +const SRC_DIR = path.join(__dirname, '../src'); +const DIST_DIR = path.join(__dirname, '../dist'); + +module.exports = (env, argv) => { + const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR }); + + return merge(commonConfig, { + devtool: 'source-map', + stats: { + colors: true, + hash: true, + timings: true, + assets: true, + chunks: false, + chunkModules: false, + modules: false, + children: false, + warnings: true, + }, + optimization: { + minimize: true, + sideEffects: true, + }, + output: { + path: ROOT_DIR, + library: 'OHIFExtCornerstone', + libraryTarget: 'umd', + libraryExport: 'default', + filename: pkg.main, + }, + plugins: [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + }); +}; diff --git a/modes/SRViewportExample/LICENSE b/modes/SRViewportExample/LICENSE new file mode 100644 index 000000000..19e20dd35 --- /dev/null +++ b/modes/SRViewportExample/LICENSE @@ -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. diff --git a/modes/SRViewportExample/babel.config.js b/modes/SRViewportExample/babel.config.js new file mode 100644 index 000000000..325ca2a8e --- /dev/null +++ b/modes/SRViewportExample/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config.js'); diff --git a/modes/SRViewportExample/package.json b/modes/SRViewportExample/package.json new file mode 100644 index 000000000..e54e85f1d --- /dev/null +++ b/modes/SRViewportExample/package.json @@ -0,0 +1,35 @@ +{ + "name": "@ohif/sr-mode-example", + "version": "0.0.1", + "description": "Example SR mode for OHIF", + "author": "OHIF", + "license": "MIT", + "repository": "OHIF/Viewers", + "main": "dist/index.umd.js", + "module": "src/index.js", + "engines": { + "node": ">=10", + "npm": ">=6", + "yarn": ">=1.16.0" + }, + "files": [ + "dist", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --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" + }, + "peerDependencies": {}, + "dependencies": { + "@babel/runtime": "7.7.6" + } +} diff --git a/modes/SRViewportExample/src/index.js b/modes/SRViewportExample/src/index.js new file mode 100644 index 000000000..cb5390881 --- /dev/null +++ b/modes/SRViewportExample/src/index.js @@ -0,0 +1,104 @@ +export default function mode({ modeConfiguration }) { + return { + id: 'sr-example-mode', + validationTags: { + study: [], + series: [], + }, + isValidMode: (studyTags, seriesTags) => { + // All series are welcome in this mode! + return true; + }, + routes: [ + { + path: 'viewer', + init: ({ servicesManager, extensionManager }) => { + const { ToolBarService } = servicesManager.services; + ToolBarService.init(extensionManager); + ToolBarService.addButtons([ + { + id: 'Zoom', + namespace: 'org.ohif.cornerstone.toolbarModule.Zoom', + }, + { + id: 'Levels', + namespace: 'org.ohif.cornerstone.toolbarModule.Wwwc', + }, + { + id: 'Pan', + namespace: 'org.ohif.cornerstone.toolbarModule.Pan', + }, + { + id: 'Capture', + namespace: 'org.ohif.cornerstone.toolbarModule.Capture', + }, + { + id: 'Layout', + namespace: 'org.ohif.default.toolbarModule.Layout', + }, + { + id: 'Annotate', + namespace: 'org.ohif.cornerstone.toolbarModule.Annotate', + }, + { + id: 'Bidirectional', + namespace: 'org.ohif.cornerstone.toolbarModule.Bidirectional', + }, + { + id: 'Ellipse', + namespace: 'org.ohif.cornerstone.toolbarModule.Ellipse', + }, + { + id: 'Length', + namespace: 'org.ohif.cornerstone.toolbarModule.Length', + }, + ]); + + // Could import layout selector here from org.ohif.default (when it exists!) + ToolBarService.setToolBarLayout([ + // Primary + { + tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'], + moreTools: ['Zoom'], + }, + // Secondary + { + tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'], + }, + ]); + }, + layoutTemplate: ({ routeProps }) => { + return { + id: 'org.ohif.default.layoutTemplateModule.viewerLayout', + props: { + // named slots + leftPanels: ['org.ohif.default.panelModule.seriesList'], + rightPanels: ['org.ohif.default.panelModule.measure'], + viewports: [ + { + namespace: 'org.ohif.cornerstone.viewportModule.cornerstone', + displaySetsToDisplay: [ + 'org.ohif.default.sopClassHandlerModule.stack', + ], + }, + { + namespace: 'org.ohif.dicom-sr.viewportModule.dicom-sr', + displaySetsToDisplay: [ + 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr', + ], + }, + ], + }, + }; + }, + }, + ], + extensions: ['org.ohif.default', 'org.ohif.cornerstone'], + sopClassHandlers: [ + 'org.ohif.default.sopClassHandlerModule.stack', + 'org.ohif.dicom-sr.sopClassHandlerModule.dicom-sr', + ], + }; +} + +window.SRViewportExample = mode({}); diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 4c22092c5..f35340229 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -59,6 +59,7 @@ "@ohif/extension-dicom-pdf": "^1.0.1", "@ohif/extension-lesion-tracker": "^0.2.0", "@ohif/extension-measurement-tracking": "^0.0.1", + "@ohif/extension-dicom-sr": "^0.0.1", "@ohif/extension-vtk": "^1.5.6", "@ohif/i18n": "^0.52.8", "@ohif/mode-longitudinal": "^0.0.1", diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 22e3c23c0..52f988e5e 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -19,6 +19,7 @@ import appInit from './appInit.js'; // TODO: Temporarily for testing import '@ohif/mode-example'; +import '@ohif/sr-mode-example'; import '@ohif/mode-longitudinal'; /** @@ -48,7 +49,12 @@ function App({ config, defaultExtensions }) { extensionManager, servicesManager ); - const { UIDialogService, UIModalService, UINotificationService } = servicesManager.services; + + const { + UIDialogService, + UIModalService, + UINotificationService, + } = servicesManager.services; // A UI Service may need to use the ViewportGrid context const viewportGridReducer = (state, action) => { diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index 4d236ee73..973bb7603 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -67,6 +67,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { if (!appConfig.modes.length) { appConfig.modes.push(window.exampleMode); appConfig.modes.push(window.longitudinalMode); + appConfig.modes.push(window.SRViewportExample); } return { diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index 89446f8e2..fa7a6c7e4 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -12,6 +12,7 @@ function ViewerViewportGrid(props) { { numCols, numRows, activeViewportIndex, viewports }, dispatch, ] = useViewportGrid(); + const setActiveViewportIndex = index => { dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }); }; @@ -87,6 +88,7 @@ function ViewerViewportGrid(props) { const viewportIndex = i; const paneMetadata = viewports[i] || {}; const { displaySetInstanceUID } = paneMetadata; + const displaySet = DisplaySetService.getDisplaySetByUID(displaySetInstanceUID) || {}; const ViewportComponent = _getViewportComponent( diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js index c742a5c95..437b9ddf4 100644 --- a/platform/viewer/src/index.js +++ b/platform/viewer/src/index.js @@ -24,6 +24,7 @@ import ReactDOM from 'react-dom'; import OHIFDefaultExtension from '@ohif/extension-default'; import OHIFCornerstoneExtension from '@ohif/extension-cornerstone'; import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking'; +import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr'; /** Combine our appConfiguration and "baked-in" extensions */ const appProps = { @@ -32,6 +33,7 @@ const appProps = { OHIFDefaultExtension, OHIFCornerstoneExtension, OHIFMeasurementTrackingExtension, + OHIFDICOMSRExtension, ], }; diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index a728934ea..50d29fb1e 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -2,11 +2,8 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router'; import PropTypes from 'prop-types'; // TODO: DicomMetadataStore should be injected? -import { DicomMetadataStore, ToolBarManager } from '@ohif/core'; -import { - DragAndDropProvider, - ImageViewerProvider, -} from '@ohif/ui'; +import { DicomMetadataStore } from '@ohif/core'; +import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui'; // import { useQuery } from '@hooks'; import ViewportGrid from '@components/ViewportGrid'; @@ -131,8 +128,8 @@ export default function ModeRoute({ > {/* TODO: extensionManager is already provided to the extension module. - * Use it from there instead of passing as a prop here. - */} + * Use it from there instead of passing as a prop here. + */}