From 92314513f6f54aa27d23de0f6e94c9604260a866 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:20:14 -0400 Subject: [PATCH 1/6] Delete "basic" example mode, replace with segmentation mode --- .../.webpack/webpack.dev.js | 0 .../.webpack/webpack.prod.js | 0 modes/{example => segmentation}/LICENSE | 0 modes/{example => segmentation}/babel.config.js | 0 modes/{example => segmentation}/package.json | 4 ++-- modes/{example => segmentation}/src/index.js | 15 +++++++-------- .../src/toolbarButtons.js | 0 7 files changed, 9 insertions(+), 10 deletions(-) rename modes/{example => segmentation}/.webpack/webpack.dev.js (100%) rename modes/{example => segmentation}/.webpack/webpack.prod.js (100%) rename modes/{example => segmentation}/LICENSE (100%) rename modes/{example => segmentation}/babel.config.js (100%) rename modes/{example => segmentation}/package.json (90%) rename modes/{example => segmentation}/src/index.js (88%) rename modes/{example => segmentation}/src/toolbarButtons.js (100%) diff --git a/modes/example/.webpack/webpack.dev.js b/modes/segmentation/.webpack/webpack.dev.js similarity index 100% rename from modes/example/.webpack/webpack.dev.js rename to modes/segmentation/.webpack/webpack.dev.js diff --git a/modes/example/.webpack/webpack.prod.js b/modes/segmentation/.webpack/webpack.prod.js similarity index 100% rename from modes/example/.webpack/webpack.prod.js rename to modes/segmentation/.webpack/webpack.prod.js diff --git a/modes/example/LICENSE b/modes/segmentation/LICENSE similarity index 100% rename from modes/example/LICENSE rename to modes/segmentation/LICENSE diff --git a/modes/example/babel.config.js b/modes/segmentation/babel.config.js similarity index 100% rename from modes/example/babel.config.js rename to modes/segmentation/babel.config.js diff --git a/modes/example/package.json b/modes/segmentation/package.json similarity index 90% rename from modes/example/package.json rename to modes/segmentation/package.json index e98a3601e..66ba594e8 100644 --- a/modes/example/package.json +++ b/modes/segmentation/package.json @@ -1,7 +1,7 @@ { - "name": "@ohif/mode-example", + "name": "@ohif/mode-segmentation", "version": "0.0.1", - "description": "Example mode for OHIF", + "description": "Segmentation mode for OHIF", "author": "OHIF", "license": "MIT", "repository": "OHIF/Viewers", diff --git a/modes/example/src/index.js b/modes/segmentation/src/index.js similarity index 88% rename from modes/example/src/index.js rename to modes/segmentation/src/index.js index 597f179ca..fc56ce4ed 100644 --- a/modes/example/src/index.js +++ b/modes/segmentation/src/index.js @@ -3,8 +3,10 @@ import { hotkeys } from '@ohif/core'; export default function mode({ modeConfiguration }) { return { - id: 'example-mode', - displayName: 'Basic Viewer', + // TODO: Mode uses 'id' for route when it should use `slug`, if provided, and + // the route path + id: 'segmentation', + displayName: 'Segmentation', validationTags: { study: [], series: [], @@ -15,7 +17,7 @@ export default function mode({ modeConfiguration }) { }, routes: [ { - path: 'viewer', + path: 'segmentation', init: ({ servicesManager, extensionManager }) => { const { ToolBarService } = servicesManager.services; ToolBarService.init(extensionManager); @@ -51,7 +53,6 @@ export default function mode({ modeConfiguration }) { return { id: 'org.ohif.default.layoutTemplateModule.viewerLayout', props: { - // named slots leftPanels: ['org.ohif.default.panelModule.seriesList'], rightPanels: ['org.ohif.default.panelModule.measure'], viewports: [ @@ -69,10 +70,8 @@ export default function mode({ modeConfiguration }) { ], extensions: ['org.ohif.default', 'org.ohif.cornerstone'], sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'], - hotkeys: [ - ...hotkeys.defaults.hotkeyBindings - ] + hotkeys: [...hotkeys.defaults.hotkeyBindings], }; } -window.exampleMode = mode({}); +window.segmentationMode = mode({}); diff --git a/modes/example/src/toolbarButtons.js b/modes/segmentation/src/toolbarButtons.js similarity index 100% rename from modes/example/src/toolbarButtons.js rename to modes/segmentation/src/toolbarButtons.js From e82b6b3c5e798dd7f3684bf7d3b284825275b9b1 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:30:47 -0400 Subject: [PATCH 2/6] Rename longitudinal mode to "Basic Viewer"; matching design --- modes/longitudinal/src/index.js | 8 +++----- platform/viewer/src/App.jsx | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 2bf100c09..db09204ba 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -20,8 +20,8 @@ export default function mode({ modeConfiguration }) { return { // TODO: We're using this as a route segment // We should not be. - id: 'longitudinal-workflow', - displayName: 'Comparison', + id: 'viewer', + displayName: 'Basic Viewer', validationTags: { study: [], series: [], @@ -95,9 +95,7 @@ export default function mode({ modeConfiguration }) { 'org.ohif.dicom-sr', ], sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler], - hotkeys: [ - ...hotkeys.defaults.hotkeyBindings - ] + hotkeys: [...hotkeys.defaults.hotkeyBindings], }; } diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 1cb815d60..1e72bd772 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -20,8 +20,8 @@ import createRoutes from './routes'; import appInit from './appInit.js'; // TODO: Temporarily for testing -import '@ohif/mode-example'; import '@ohif/mode-longitudinal'; +import '@ohif/mode-segmentation'; /** * ENV Variable to determine routing behavior @@ -50,7 +50,7 @@ function App({ config, defaultExtensions }) { dataSources, extensionManager, servicesManager, - hotkeysManager + hotkeysManager, }); const { UIDialogService, From eb0f38ebf25df7c53e0dddb4115a5e0353f9b1ee Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:48:24 -0400 Subject: [PATCH 3/6] Prevent default measurementpanel crash --- extensions/default/src/ActionButtons.jsx | 37 ----- .../default/src/PanelMeasurementTable.js | 150 ++++++++++++++---- 2 files changed, 115 insertions(+), 72 deletions(-) delete mode 100644 extensions/default/src/ActionButtons.jsx diff --git a/extensions/default/src/ActionButtons.jsx b/extensions/default/src/ActionButtons.jsx deleted file mode 100644 index 0e3e8a3b6..000000000 --- a/extensions/default/src/ActionButtons.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui'; - -function ActionButtons() { - return ( - - alert('Export')}> - - - - - - - - ); -} - -export default ActionButtons; diff --git a/extensions/default/src/PanelMeasurementTable.js b/extensions/default/src/PanelMeasurementTable.js index 6da4696fd..cd83598b2 100644 --- a/extensions/default/src/PanelMeasurementTable.js +++ b/extensions/default/src/PanelMeasurementTable.js @@ -1,55 +1,135 @@ -import React from 'react'; -import { StudySummary, MeasurementTable } from '@ohif/ui'; -import ActionButtons from './ActionButtons.jsx'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { MeasurementTable } from '@ohif/ui'; +import { DicomMetadataStore } from '@ohif/core'; export default function PanelMeasurementTable({ servicesManager, - commandsManager, + // commandsManager, }) { const { MeasurementService } = servicesManager.services; + const [displayMeasurements, setDisplayMeasurements] = useState([]); - console.log('MeasurementTable rendering!!!!!!!!!!!!!'); + useEffect(() => { + const measurements = MeasurementService.getMeasurements(); + const mappedMeasurements = measurements.map((m, index) => + _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) + ); + setDisplayMeasurements(mappedMeasurements); + }, [MeasurementService]); - const descriptionData = { - date: '07-Sep-2010', - modality: 'CT', - description: 'CHEST/ABD/PELVIS W CONTRAST', - }; - - const activeMeasurementItem = 0; - - const measurementTableData = { - title: 'Measurements', - amount: 10, - data: new Array(10).fill({}).map((el, i) => ({ - id: i + 1, - label: 'Label short description', - displayText: '24.0 x 24.0 mm (S:4, I:22)', - isActive: activeMeasurementItem === i + 1, - })), - onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)), - onEdit: id => alert(`Edit: ${id}`), - }; + // const activeMeasurementItem = 0; return ( <>
- {}} onEdit={id => alert(`Edit: ${id}`)} />
-
- -
); } + +PanelMeasurementTable.propTypes = { + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + MeasurementService: PropTypes.shape({ + getMeasurements: PropTypes.func.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; + +function _mapMeasurementToDisplay(measurement, index, types) { + const { + id, + label, + description, + // Reference IDs + referenceStudyUID, + referenceSeriesUID, + SOPInstanceUID, + } = measurement; + const instance = DicomMetadataStore.getInstance( + referenceStudyUID, + referenceSeriesUID, + SOPInstanceUID + ); + const { PixelSpacing, SeriesNumber, InstanceNumber } = instance; + + return { + id: index + 1, + label: '(empty)', // 'Label short description', + displayText: + _getDisplayText( + measurement, + PixelSpacing, + SeriesNumber, + InstanceNumber, + types + ) || [], + // TODO: handle one layer down + isActive: false, // activeMeasurementItem === i + 1, + }; +} + +function _getDisplayText( + measurement, + pixelSpacing, + seriesNumber, + instanceNumber, + types +) { + const { type, points } = measurement; + const hasPixelSpacing = + pixelSpacing !== undefined && + Array.isArray(pixelSpacing) && + pixelSpacing.length === 2; + const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing + ? pixelSpacing + : [1, 1]; + const unit = hasPixelSpacing ? 'mm' : 'px'; + + switch (type) { + case types.POLYLINE: { + const { length } = measurement; + const roundedLength = _round(length, 1); + + return [ + `${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, + ]; + } + case types.BIDIRECTIONAL: { + const { shortestDiameter, longestDiameter } = measurement; + const roundedShortestDiameter = _round(shortestDiameter, 1); + const roundedLongestDiameter = _round(longestDiameter, 1); + + return [ + `l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, + `s: ${roundedShortestDiameter} ${unit}`, + ]; + } + case types.ELLIPSE: { + const { area } = measurement; + const roundedArea = _round(area, 1); + + return [ + `${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`, + ]; + } + case types.POINT: { + const { text } = measurement; + return [`${text} (S:${seriesNumber}, I:${instanceNumber})`]; + } + } +} + +function _round(value, decimals) { + return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); +} From e3ced7e29455f871f01e6f5fcf71cea66b0b4f92 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:48:42 -0400 Subject: [PATCH 4/6] Add segmentation mode at appInit --- platform/viewer/src/appInit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index eb14613ce..3c35dd487 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -71,8 +71,8 @@ function appInit(appConfigOrFunc, defaultExtensions) { // TODO: Remove this if (!appConfig.modes.length) { - appConfig.modes.push(window.exampleMode); appConfig.modes.push(window.longitudinalMode); + appConfig.modes.push(window.segmentationMode); } return { @@ -80,7 +80,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { commandsManager, extensionManager, servicesManager, - hotkeysManager + hotkeysManager, }; } From 064df8a5098ec776a37438fad11bea35523042a3 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:49:05 -0400 Subject: [PATCH 5/6] Fixing lint errors in PanelMeasurementTableTracking --- .../PanelMeasurementTableTracking/index.js | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 9a40f8ca5..e8381f39f 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -21,7 +21,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services; + const { + MeasurementService, + UINotificationService, + UIDialogService, + DisplaySetService, + } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -31,9 +36,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DISPLAY_STUDY_SUMMARY_INITIAL_VALUE ); const [displayMeasurements, setDisplayMeasurements] = useState([]); - // TODO: measurements subscribtion - // Initial? useEffect(() => { const measurements = MeasurementService.getMeasurements(); const filteredMeasurements = measurements.filter( @@ -120,7 +123,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { isDraggable: false, centralize: true, // TODO: Create a loading indicator component + zeplin design? - content: () =>
Loading...
+ content: () =>
Loading...
, }); try { @@ -136,13 +139,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { // Would need some way of saying which one is the "push" dataSource const dataSource = dataSources[0]; - const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + const naturalizedReport = await DICOMSR.storeMeasurements( + trackedMeasurements, + dataSource + ); - DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true }); + DisplaySetService.makeDisplaySets([naturalizedReport], { + madeInClient: true, + }); UINotificationService.show({ title: 'STOW SR', message: 'Measurements saved successfully', - type: 'success' + type: 'success', }); } catch (error) { UINotificationService.show({ @@ -169,7 +177,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { title="Measurements" amount={displayMeasurements.length} data={displayMeasurements} - onClick={() => { }} + onClick={() => {}} onEdit={id => alert(`Edit: ${id}`)} /> @@ -183,7 +191,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { ); } -PanelMeasurementTableTracking.propTypes = {}; +PanelMeasurementTableTracking.propTypes = { + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + MeasurementService: PropTypes.shape({ + getMeasurements: PropTypes.func.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; // TODO: This could be a MeasurementService mapper function _mapMeasurementToDisplay(measurement, index, types) { @@ -203,9 +220,6 @@ function _mapMeasurementToDisplay(measurement, index, types) { ); const { PixelSpacing, SeriesNumber, InstanceNumber } = instance; - console.log('mapping....', measurement); - console.log(instance); - return { id: index + 1, label: '(empty)', // 'Label short description', @@ -234,15 +248,7 @@ function _getDisplayText( instanceNumber, types ) { - // TODO: determination of shape influences text - // Length: 'xx.x unit (S:x, I:x)' - // Rectangle: 'xx.x x xx.x unit (S:x, I:x)', - // Ellipse? - // Bidirectional? - // Freehand? - const { type, points } = measurement; - const hasPixelSpacing = pixelSpacing !== undefined && Array.isArray(pixelSpacing) && @@ -253,18 +259,16 @@ function _getDisplayText( const unit = hasPixelSpacing ? 'mm' : 'px'; switch (type) { - case types.POLYLINE: + case types.POLYLINE: { const { length } = measurement; - const roundedLength = _round(length, 1); return [ `${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, ]; - - case types.BIDIRECTIONAL: + } + case types.BIDIRECTIONAL: { const { shortestDiameter, longestDiameter } = measurement; - const roundedShortestDiameter = _round(shortestDiameter, 1); const roundedLongestDiameter = _round(longestDiameter, 1); @@ -272,16 +276,19 @@ function _getDisplayText( `l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, `s: ${roundedShortestDiameter} ${unit}`, ]; - case types.ELLIPSE: + } + case types.ELLIPSE: { const { area } = measurement; - const roundedArea = _round(area, 1); + return [ `${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`, ]; - case types.POINT: + } + case types.POINT: { const { text } = measurement; return [`${text} (S:${seriesNumber}, I:${instanceNumber})`]; + } } } From 236b89a5515c89fd17f3deee792f153eeee2960b Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 16:04:07 -0400 Subject: [PATCH 6/6] Make sure measurements table updates --- .../default/src/PanelMeasurementTable.js | 43 +++++++++++++++++-- extensions/default/src/debounce.js | 21 +++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 extensions/default/src/debounce.js diff --git a/extensions/default/src/PanelMeasurementTable.js b/extensions/default/src/PanelMeasurementTable.js index cd83598b2..0967ac378 100644 --- a/extensions/default/src/PanelMeasurementTable.js +++ b/extensions/default/src/PanelMeasurementTable.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { MeasurementTable } from '@ohif/ui'; import { DicomMetadataStore } from '@ohif/core'; +import debounce from './debounce.js'; export default function PanelMeasurementTable({ servicesManager, @@ -11,11 +12,34 @@ export default function PanelMeasurementTable({ const [displayMeasurements, setDisplayMeasurements] = useState([]); useEffect(() => { - const measurements = MeasurementService.getMeasurements(); - const mappedMeasurements = measurements.map((m, index) => - _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) + const debouncedSetDisplayMeasurements = debounce( + setDisplayMeasurements, + 100 ); - setDisplayMeasurements(mappedMeasurements); + // ~~ Initial + setDisplayMeasurements(_getMappedMeasurements(MeasurementService)); + + // ~~ Subscription + const added = MeasurementService.EVENTS.MEASUREMENT_ADDED; + const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED; + const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED; + const subscriptions = []; + + [added, updated, removed].forEach(evt => { + subscriptions.push( + MeasurementService.subscribe(evt, () => { + debouncedSetDisplayMeasurements( + _getMappedMeasurements(MeasurementService) + ); + }).unsubscribe + ); + }); + + return () => { + subscriptions.forEach(unsub => { + unsub(); + }); + }; }, [MeasurementService]); // const activeMeasurementItem = 0; @@ -40,12 +64,23 @@ PanelMeasurementTable.propTypes = { services: PropTypes.shape({ MeasurementService: PropTypes.shape({ getMeasurements: PropTypes.func.isRequired, + subscribe: PropTypes.func.isRequired, + EVENTS: PropTypes.object.isRequired, VALUE_TYPES: PropTypes.object.isRequired, }).isRequired, }).isRequired, }).isRequired, }; +function _getMappedMeasurements(MeasurementService) { + const measurements = MeasurementService.getMeasurements(); + const mappedMeasurements = measurements.map((m, index) => + _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) + ); + + return mappedMeasurements; +} + function _mapMeasurementToDisplay(measurement, index, types) { const { id, diff --git a/extensions/default/src/debounce.js b/extensions/default/src/debounce.js new file mode 100644 index 000000000..83a16b099 --- /dev/null +++ b/extensions/default/src/debounce.js @@ -0,0 +1,21 @@ +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, + args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} + +export default debounce;