From 1bfb62d26d2d9ab9486fe25fff7c7f7da0ba7e47 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 12:33:38 -0300 Subject: [PATCH 01/24] Add messages to sr pacs --- .../PanelMeasurementTableTracking/index.js | 47 +++-- platform/ui/src/assets/icons/close.svg | 2 +- .../src/components/IconButton/IconButton.jsx | 2 +- .../ui/src/components/Snackbar/Snackbar.css | 172 ++++++++++++++++++ .../components/Snackbar/SnackbarContainer.jsx | 24 +-- .../src/components/Snackbar/SnackbarItem.jsx | 33 ++-- .../src/contextProviders/DialogProvider.css | 8 + .../src/contextProviders/DialogProvider.jsx | 35 +++- 8 files changed, 275 insertions(+), 48 deletions(-) create mode 100644 platform/ui/src/components/Snackbar/Snackbar.css create mode 100644 platform/ui/src/contextProviders/DialogProvider.css diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 10184832a..20987a38e 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -24,7 +24,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService } = servicesManager.services; + const { MeasurementService, UINotificationService, UIDialogService } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -117,21 +117,40 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DICOMSR.downloadReport(trackedMeasurements, dataSource); }; - const onCreateReportClick = () => { - const measurements = MeasurementService.getMeasurements(); - const trackedMeasurements = measurements.filter( - m => - trackedStudy === m.referenceStudyUID && - trackedSeries.includes(m.referenceSeriesUID) - ); + const onCreateReportClick = async () => { + // TODO: Create a loading service that uses the dialog service with these options? + const loadingDialogId = UIDialogService.create({ + showOverlay: true, + isDraggable: false, + centralize: true, + // TODO: Create a loading indicator component + zeplin design? + content: () =>
Loading...
+ }); + try { + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); - const dataSources = extensionManager.getDataSources(); - // TODO -> Eventually deal with multiple dataSources. - // Would need some way of saying which one is the "push" dataSource - const dataSource = dataSources[0]; + const dataSources = extensionManager.getDataSources(); + // TODO -> Eventually deal with multiple dataSources. + // Would need some way of saying which one is the "push" dataSource + const dataSource = dataSources[0]; - DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + const { message } = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + UINotificationService.show({ title: 'STOW SR', message, type: 'success' }); + } catch (error) { + UINotificationService.show({ + title: 'STOW SR', + message: error.message || 'Failed to store measurements', + type: 'error', + }); + } finally { + UIDialogService.dismiss({ id: loadingDialogId }); + } }; return ( @@ -148,7 +167,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { title="Measurements" amount={displayMeasurements.length} data={displayMeasurements} - onClick={() => {}} + onClick={() => { }} onEdit={id => alert(`Edit: ${id}`)} /> diff --git a/platform/ui/src/assets/icons/close.svg b/platform/ui/src/assets/icons/close.svg index 83b259fca..03181c71f 100644 --- a/platform/ui/src/assets/icons/close.svg +++ b/platform/ui/src/assets/icons/close.svg @@ -1,4 +1,4 @@ - + diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index 290bfa273..38b94a87f 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => {}, + onClick: () => { }, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/Snackbar/Snackbar.css b/platform/ui/src/components/Snackbar/Snackbar.css new file mode 100644 index 000000000..0c3a0546a --- /dev/null +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -0,0 +1,172 @@ +/* TODO: Create tailwind styles for this component */ +.sb-topLeft { + top: 0; + bottom: auto; + left: 0; + right: auto; +} + +.sb-topCenter { + left: 50%; + top: 0; + bottom: auto; + transform: translateX(-50%); +} + +.sb-topRight { + top: 0; + bottom: auto; + left: auto; + right: 0; +} + +.sb-bottomLeft { + top: auto; + bottom: 0px; + left: 0px; + right: auto; +} + +.sb-bottomCenter { + left: 50%; + bottom: 0; + top: auto; + transform: translateX(-50%); +} + +.sb-bottomRight { + top: auto; + bottom: 0px; + left: auto; + right: 0px; + margin: 10px 0 0; +} + +.sb-topLeft .sb-item, +.sb-topCenter .sb-item, +.sb-topRight .sb-item { + margin: 10px 0 0; +} + +.sb-bottomLeft .sb-item, +.sb-bottomCenter .sb-item, +.sb-bottomRight .sb-item { + margin: 0 0 10px; +} + +.sb-closeBtn { + height: 20px; + opacity: 1; + overflow: hidden; + padding: 2px; + text-align: center; + text-shadow: none; + width: 20px; + cursor: pointer; + position: absolute; + right: 5px; + top: 5px; + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.6); + border-radius: 100%; +} + +.sb-closeBtn:hover { + background: #fff; +} + +.sb-closeIcon { + display: block; + font-size: 0; + height: 100%; + line-height: 0; + overflow: hidden; + position: relative; + width: 100%; +} + +.sb-closeIcon:after, +.sb-closeIcon:before { + content: ' '; + display: block; + height: 2px; + transition: all 0.3s ease; + width: 12px; + background-color: #222; + opacity: 1; + position: absolute; +} + +.sb-closeIcon:before { + left: 4px; + top: 3px; + + transform: rotate(45deg); + transform-origin: 0px 50%; +} + +.sb-closeIcon:after { + right: 3px; + top: 5px; + transform: rotate(-45deg); + transform-origin: calc(100% - 3px) 50%; +} + +.sb-title { + font-size: 16px; + font-weight: bold; +} + +.sb-message { + font-size: 14px; + word-break: normal; +} + +.sb-item { + position: relative; + transition: height 300ms ease; + animation: fadein 1s; + padding: 20px; + color: white; + overflow: hidden; + border-radius: 4px; + box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12), + 0 3px 5px -1px rgba(0, 0, 0, 0.14); +} + +.sb-item a { + color: white; + text-decoration: underline; +} + +.sb-hidden { + padding-top: 0; + padding-bottom: 0; + margin-bottom: 0; + height: 0; + opacity: 0; + transition: all 300ms ease; +} + +@keyframes fadein { + from { + opacity: 0; + top: 30px; + } + to { + opacity: 1; + top: 0; + } +} + +/* Internet Explorer */ +@-ms-keyframes fadein { + from { + opacity: 0; + top: 30px; + } + to { + opacity: 1; + top: 0; + } +} diff --git a/platform/ui/src/components/Snackbar/SnackbarContainer.jsx b/platform/ui/src/components/Snackbar/SnackbarContainer.jsx index eb0e83fb2..40c76fc00 100644 --- a/platform/ui/src/components/Snackbar/SnackbarContainer.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarContainer.jsx @@ -2,16 +2,18 @@ import React from 'react'; import SnackbarItem from './SnackbarItem'; import { useSnackbar } from '../../contextProviders'; +import './Snackbar.css'; + const SnackbarContainer = () => { const { snackbarItems, hide } = useSnackbar(); - const renderItem = item => { - return ; - }; - - if (!snackbarItems) { - return null; - } + const renderItem = item => ( + + ); const renderItems = () => { const items = { @@ -23,11 +25,9 @@ const SnackbarContainer = () => { bottomRight: [], }; - snackbarItems.map(item => { - items[item.position].push(item); - }); + snackbarItems.forEach(item => items[item.position].push(item)); - return ( + return snackbarItems && (
{Object.keys(items).map(pos => { if (!items[pos].length) { @@ -35,7 +35,7 @@ const SnackbarContainer = () => { } return ( -
+
{items[pos].map((item, index) => (
{renderItem(item)}
))} diff --git a/platform/ui/src/components/Snackbar/SnackbarItem.jsx b/platform/ui/src/components/Snackbar/SnackbarItem.jsx index abeef2ccf..dc0f2bcef 100644 --- a/platform/ui/src/components/Snackbar/SnackbarItem.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarItem.jsx @@ -1,25 +1,36 @@ import React, { useEffect } from 'react'; +import classNames from 'classnames'; + +import SnackbarTypes from './SnackbarTypes'; const SnackbarItem = ({ options, onClose }) => { - const handleClose = () => { - onClose(options.id); - }; + const handleClose = () => onClose(options.id); useEffect(() => { if (options.autoClose) { - setTimeout(() => { - handleClose(); - }, options.duration); + setTimeout(() => handleClose(), options.duration); } }, []); + const typeClasses = { + [SnackbarTypes.INFO]: 'bg-primary-active', + [SnackbarTypes.WARNING]: 'bg-yellow-600', + [SnackbarTypes.SUCCESS]: 'bg-green-600', + [SnackbarTypes.ERROR]: 'bg-red-600' + }; + return ( -
- - x +
+ + x - {options.title &&
{options.title}
} - {options.message &&
{options.message}
} + {options.title &&
{options.title}
} + {options.message &&
{options.message}
}
); }; diff --git a/platform/ui/src/contextProviders/DialogProvider.css b/platform/ui/src/contextProviders/DialogProvider.css new file mode 100644 index 000000000..09438e95b --- /dev/null +++ b/platform/ui/src/contextProviders/DialogProvider.css @@ -0,0 +1,8 @@ +/* TODO: Find a better way to set the cursor for all contents of dialog. */ +.DraggableItem.draggable div { + cursor: grab !important; +} + +.DraggableItem.draggable.dragging div { + cursor: grabbing !important; +} diff --git a/platform/ui/src/contextProviders/DialogProvider.jsx b/platform/ui/src/contextProviders/DialogProvider.jsx index eb3c207cb..58ed46432 100644 --- a/platform/ui/src/contextProviders/DialogProvider.jsx +++ b/platform/ui/src/contextProviders/DialogProvider.jsx @@ -5,11 +5,14 @@ import React, { useCallback, useEffect, } from 'react'; + import PropTypes from 'prop-types'; import Draggable from 'react-draggable'; import classNames from 'classnames'; + import { utils } from '@ohif/core'; +import './DialogProvider.css'; const DialogContext = createContext(null); @@ -150,6 +153,7 @@ const DialogProvider = ({ children, service }) => { onStart, onStop, onDrag, + showOverlay, } = dialog; let position = @@ -158,13 +162,13 @@ const DialogProvider = ({ children, service }) => { position = centerPositions.find(position => position.id === id); } - return ( + const dragableItem = () => ( { const e = event || window.event; const target = e.target || e.srcElement; @@ -215,6 +219,21 @@ const DialogProvider = ({ children, service }) => {
); + + const withOverlay = component => { + const background = 'bg-black bg-opacity-50'; + const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto'; + return ( +
+ {component} +
+ ); + }; + + return showOverlay ? withOverlay(dragableItem()) : dragableItem(); }); /** @@ -236,13 +255,11 @@ const DialogProvider = ({ children, service }) => { return ( -
- {dialogs.some(dialog => dialog.showOverlay) ? ( -
{renderDialogs()}
- ) : ( - renderDialogs() - )} -
+ {!isEmpty() && +
+ {renderDialogs()} +
+ } {children}
); From 5ca6757b449b0eb0adafabd1e3c1bd423ae83435 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 14:02:23 -0300 Subject: [PATCH 02/24] Add error message of generate reportt --- .../PanelMeasurementTableTracking/index.js | 17 +++++++---------- platform/core/src/DICOMSR/dataExchange.js | 19 +++++++------------ .../ui/src/components/Snackbar/Snackbar.css | 14 -------------- .../src/components/Snackbar/SnackbarItem.jsx | 4 +++- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index e6473ed68..9a40f8ca5 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -136,17 +136,14 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { // Would need some way of saying which one is the "push" dataSource const dataSource = dataSources[0]; - const { message } = await DICOMSR.storeMeasurements( - trackedMeasurements, - dataSource, - naturalizedReport => { - DisplaySetService.makeDisplaySets([naturalizedReport], { - madeInClient: true, - }); - } - ); + const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource); - UINotificationService.show({ title: 'STOW SR', message, type: 'success' }); + DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true }); + UINotificationService.show({ + title: 'STOW SR', + message: 'Measurements saved successfully', + type: 'success' + }); } catch (error) { UINotificationService.show({ title: 'STOW SR', diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index 8f86f743c..b0dad9044 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -79,8 +79,9 @@ const generateReport = measurementData => { * @param {object[]} measurementData An array of measurements from the measurements service * that you wish to serialize. * @param {object} dataSource The dataSource that you wish to use to persist the data. + * @return {object} The naturalized report */ -const storeMeasurements = async (measurementData, dataSource, onSuccess) => { +const storeMeasurements = async (measurementData, dataSource) => { // TODO -> Eventually use the measurements directly and not the dcmjs adapter, // But it is good enough for now whilst we only have cornerstone as a datasource. log.info('[DICOMSR] storeMeasurements'); @@ -90,28 +91,22 @@ const storeMeasurements = async (measurementData, dataSource, onSuccess) => { return Promise.reject({}); } - const naturalizedReport = generateReport(measurementData); - const { StudyInstanceUID } = naturalizedReport; - try { + const naturalizedReport = generateReport(measurementData); + const { StudyInstanceUID } = naturalizedReport; + await dataSource.store.dicom(naturalizedReport); if (StudyInstanceUID) { dataSource.deleteStudyMetadataPromise(StudyInstanceUID); } - if (onSuccess) { - onSuccess(naturalizedReport); - } - - return { - message: 'Measurements saved successfully', - }; + return naturalizedReport; } catch (error) { log.error( `[DICOMSR] Error while saving the measurements: ${error.message}` ); - throw new Error('Error while saving the measurements.'); + throw new Error(error.message || 'Error while saving the measurements.'); } }; diff --git a/platform/ui/src/components/Snackbar/Snackbar.css b/platform/ui/src/components/Snackbar/Snackbar.css index 0c3a0546a..93db56bfd 100644 --- a/platform/ui/src/components/Snackbar/Snackbar.css +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -134,20 +134,6 @@ 0 3px 5px -1px rgba(0, 0, 0, 0.14); } -.sb-item a { - color: white; - text-decoration: underline; -} - -.sb-hidden { - padding-top: 0; - padding-bottom: 0; - margin-bottom: 0; - height: 0; - opacity: 0; - transition: all 300ms ease; -} - @keyframes fadein { from { opacity: 0; diff --git a/platform/ui/src/components/Snackbar/SnackbarItem.jsx b/platform/ui/src/components/Snackbar/SnackbarItem.jsx index dc0f2bcef..1c45790f2 100644 --- a/platform/ui/src/components/Snackbar/SnackbarItem.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarItem.jsx @@ -19,10 +19,12 @@ const SnackbarItem = ({ options, onClose }) => { [SnackbarTypes.ERROR]: 'bg-red-600' }; + const hidden = 'duration-300 transition-all ease-in-out h-0 opacity-0 pt-0 mb-0 pb-0'; + return (
From 04baaf941b9fee78749c2a0dd75b30fa27189b62 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 14:25:10 -0300 Subject: [PATCH 03/24] Update snackbar styles --- .../ui/src/components/Snackbar/Snackbar.css | 77 ++++--------------- platform/ui/tailwind.config.js | 2 + 2 files changed, 19 insertions(+), 60 deletions(-) diff --git a/platform/ui/src/components/Snackbar/Snackbar.css b/platform/ui/src/components/Snackbar/Snackbar.css index 93db56bfd..0f0083def 100644 --- a/platform/ui/src/components/Snackbar/Snackbar.css +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -1,45 +1,29 @@ /* TODO: Create tailwind styles for this component */ .sb-topLeft { - top: 0; - bottom: auto; - left: 0; - right: auto; + @apply top-0 left-0 bottom-auto right-auto; } .sb-topCenter { - left: 50%; - top: 0; - bottom: auto; transform: translateX(-50%); + @apply top-0 bottom-auto left-1/2; } .sb-topRight { - top: 0; - bottom: auto; - left: auto; - right: 0; + @apply right-0 top-0 left-auto bottom-auto; } .sb-bottomLeft { - top: auto; - bottom: 0px; - left: 0px; - right: auto; + @apply right-auto left-0 bottom-0 top-auto; } .sb-bottomCenter { - left: 50%; - bottom: 0; - top: auto; + @apply top-auto bottom-0 left-1/2; transform: translateX(-50%); } .sb-bottomRight { - top: auto; - bottom: 0px; - left: auto; - right: 0px; margin: 10px 0 0; + @apply top-auto bottom-0 left-auto right-0; } .sb-topLeft .sb-item, @@ -55,20 +39,12 @@ } .sb-closeBtn { - height: 20px; - opacity: 1; - overflow: hidden; - padding: 2px; - text-align: center; text-shadow: none; width: 20px; - cursor: pointer; - position: absolute; + height: 20px; right: 5px; top: 5px; - transition: all 0.3s ease; - background: rgba(255, 255, 255, 0.6); - border-radius: 100%; + @apply overflow-hidden opacity-100 rounded-full p-1 bg-white cursor-pointer absolute text-center duration-300 transition-all ease-in-out; } .sb-closeBtn:hover { @@ -76,31 +52,20 @@ } .sb-closeIcon { - display: block; - font-size: 0; - height: 100%; - line-height: 0; - overflow: hidden; - position: relative; - width: 100%; + @apply w-full relative overflow-hidden h-full block leading-none; } .sb-closeIcon:after, .sb-closeIcon:before { content: ' '; - display: block; height: 2px; - transition: all 0.3s ease; width: 12px; - background-color: #222; - opacity: 1; - position: absolute; + @apply duration-300 transition-all ease-in-out block bg-black opacity-100 absolute; } .sb-closeIcon:before { left: 4px; top: 3px; - transform: rotate(45deg); transform-origin: 0px 50%; } @@ -113,46 +78,38 @@ } .sb-title { - font-size: 16px; - font-weight: bold; + @apply break-normal text-lg font-bold; } .sb-message { - font-size: 14px; - word-break: normal; + @apply break-normal text-base; } .sb-item { - position: relative; - transition: height 300ms ease; animation: fadein 1s; - padding: 20px; - color: white; - overflow: hidden; - border-radius: 4px; box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.14); + @apply relative p-5 text-white overflow-hidden rounded-md transition-height ease-in-out duration-300; } @keyframes fadein { from { - opacity: 0; top: 30px; + @apply opacity-0; } to { - opacity: 1; - top: 0; + @apply opacity-100 top-0; } } /* Internet Explorer */ @-ms-keyframes fadein { from { - opacity: 0; top: 30px; + @apply opacity-0; } to { - opacity: 1; + @apply opacity-100; top: 0; } } diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index b01fd7e1a..e70bbe86b 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -329,6 +329,7 @@ module.exports = { auto: 'auto', full: '100%', viewport: '0.5rem', + '1/2': '50%', 'viewport-scrollbar': '1.3rem' }, letterSpacing: { @@ -683,6 +684,7 @@ module.exports = { transitionProperty: { none: 'none', all: 'all', + 'height': 'height', default: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', colors: 'background-color, border-color, color, fill, stroke', From c9a365f7e24d61f83132ad692323d75039a6297f Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 15:47:09 -0300 Subject: [PATCH 04/24] double click thumbnail loads SR in viewport --- .../PanelStudyBrowserTracking.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 438ad0d06..bef7c4666 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -22,7 +22,10 @@ function PanelStudyBrowserTracking({ // doesn't have to have such an intense shape. This works well enough for now. // Tabs --> Studies --> DisplaySets --> Thumbnails const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer(); - const [{ activeViewportIndex, viewports }] = useViewportGrid(); + const [ + { activeViewportIndex, viewports }, + viewportGridService, + ] = useViewportGrid(); const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -35,6 +38,13 @@ function PanelStudyBrowserTracking({ const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); + const onClickThumbnailHandler = displaySetInstanceUID => { + viewportGridService.setDisplaysetForViewport({ + viewportIndex: activeViewportIndex, + displaySetInstanceUID, + }); + }; + // TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy? useEffect(() => { const { unsubscribe } = MeasurementService.subscribe( @@ -233,6 +243,7 @@ function PanelStudyBrowserTracking({ SeriesInstanceUID: displaySet.SeriesInstanceUID, }); }} + onClickThumbnail={onClickThumbnailHandler} /> ); } From cf30a6da88bd8f834c79e84c5933059ce7d8e15c Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 15:53:57 -0300 Subject: [PATCH 05/24] define currentDisplaySetData in viewport grid provider --- .../src/contextProviders/ViewportGridProvider.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index fe77ef0a0..eb8d9809c 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -12,6 +12,7 @@ const DEFAULT_STATE = { numCols: 1, viewports: [], activeViewportIndex: 0, + currentDisplaySetData: {}, }; export const ViewportGridContext = createContext(DEFAULT_STATE); @@ -19,15 +20,17 @@ export const ViewportGridContext = createContext(DEFAULT_STATE); export function ViewportGridProvider({ children, service }) { const viewportGridReducer = (state, action) => { switch (action.type) { - case 'SET_ACTIVE_VIEWPORT_INDEX': - return { ...state, ...{ activeViewportIndex: action.payload } }; + case 'SET_ACTIVE_VIEWPORT_INDEX': { + const activeViewportIndex = action.payload; + const currentDisplaySetData = state.viewports[activeViewportIndex]; + return { ...state, ...{ activeViewportIndex, currentDisplaySetData } }; + } case 'SET_DISPLAYSET_FOR_VIEWPORT': { const { viewportIndex, displaySetInstanceUID } = action.payload; const viewports = state.viewports.slice(); - + const currentDisplaySetData = { displaySetInstanceUID }; viewports[viewportIndex] = { displaySetInstanceUID }; - - return { ...state, ...{ viewports } }; + return { ...state, ...{ viewports, currentDisplaySetData } }; } case 'SET_LAYOUT': { const { numCols, numRows } = action.payload; @@ -55,7 +58,7 @@ export function ViewportGridProvider({ children, service }) { const [viewportGridState, dispatch] = useReducer( viewportGridReducer, - DEFAULT_STATE, + DEFAULT_STATE ); const getState = useCallback(() => viewportGridState, [viewportGridState]); From 34a9733dfefaf5734c229cb1dfa8af5102d189c9 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 15:54:21 -0300 Subject: [PATCH 06/24] replace onClick for double click and styles fixes --- .../ui/src/components/Thumbnail/Thumbnail.jsx | 3 +-- .../ThumbnailNoImage/ThumbnailNoImage.jsx | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.jsx b/platform/ui/src/components/Thumbnail/Thumbnail.jsx index 44460fa8a..2f6420f04 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.jsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.jsx @@ -36,8 +36,7 @@ const Thumbnail = ({ className, 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none' )} - onClick={onClick} - onKeyDown={onClick} + onDoubleClick={onClick} role="button" tabIndex="0" > diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 0f01bd9ce..051d4c1df 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import classnames from 'classnames'; import PropTypes from 'prop-types'; import { useDrag } from 'react-dnd'; @@ -10,6 +11,7 @@ const ThumbnailNoImage = ({ modality, onClick, dragData, + isActive, }) => { const [collectedProps, drag, dragPreview] = useDrag({ item: { ...dragData }, @@ -21,9 +23,11 @@ const ThumbnailNoImage = ({ return (
@@ -44,10 +48,22 @@ const ThumbnailNoImage = ({ }; ThumbnailNoImage.propTypes = { + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), description: PropTypes.string.isRequired, modality: PropTypes.string.isRequired, seriesDate: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, + isActive: PropTypes.bool.isRequired, }; export default ThumbnailNoImage; From f08ce57c0c36c8d92efedfb74f1e091754d6aa02 Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 15:54:34 -0300 Subject: [PATCH 07/24] pass isActive prop --- platform/ui/src/components/ThumbnailList/ThumbnailList.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index 303159df8..fa17fc13e 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -64,6 +64,7 @@ const ThumbnailList = ({ case 'thumbnailNoImage': return ( Date: Tue, 30 Jun 2020 15:55:17 -0300 Subject: [PATCH 08/24] pass currentDisplaySetUID --- .../PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index bef7c4666..cc323aae1 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -23,7 +23,7 @@ function PanelStudyBrowserTracking({ // Tabs --> Studies --> DisplaySets --> Thumbnails const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer(); const [ - { activeViewportIndex, viewports }, + { activeViewportIndex, viewports, currentDisplaySetData }, viewportGridService, ] = useViewportGrid(); const [ @@ -45,6 +45,9 @@ function PanelStudyBrowserTracking({ }); }; + const currentDisplaySetInstanceUID = + currentDisplaySetData.displaySetInstanceUID || ''; + // TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy? useEffect(() => { const { unsubscribe } = MeasurementService.subscribe( @@ -244,6 +247,7 @@ function PanelStudyBrowserTracking({ }); }} onClickThumbnail={onClickThumbnailHandler} + currentDisplaySetInstanceUID={currentDisplaySetInstanceUID} /> ); } From 90ca52585ae93c0e0aadbc1ee272eb1af72b019d Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 15:57:24 -0300 Subject: [PATCH 09/24] update thumbnail props --- .../components/StudyBrowser/StudyBrowser.jsx | 21 ++++++------------- .../ThumbnailList/ThumbnailList.jsx | 7 ++++--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx index 048489e70..24b2f52f3 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -26,9 +26,8 @@ const StudyBrowser = ({ onClickStudy, onClickThumbnail, onClickUntrack, + currentDisplaySetInstanceUID, }) => { - const [thumbnailActive, setThumbnailActive] = useState(null); - const getTabContent = () => { const tabData = tabs.find(tab => tab.name === activeTabName); @@ -58,18 +57,9 @@ const StudyBrowser = ({ {isExpanded && displaySets && ( { - setThumbnailActive( - displaySetInstanceUID === thumbnailActive - ? null - : displaySetInstanceUID - ); - onClickThumbnail(displaySetInstanceUID); - }} - onClickUntrack={displaySetInstanceUID => { - onClickUntrack(displaySetInstanceUID); - }} + currentDisplaySetInstanceUID={currentDisplaySetInstanceUID} + onThumbnailClick={onClickThumbnail} + onClickUntrack={onClickUntrack} /> )} @@ -121,6 +111,7 @@ StudyBrowser.propTypes = { onClickUntrack: PropTypes.func, activeTabName: PropTypes.string.isRequired, expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired, + currentDisplaySetInstanceUID: PropTypes.string.isRequired, tabs: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired, diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index fa17fc13e..a29a73e96 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -5,7 +5,7 @@ import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui'; const ThumbnailList = ({ thumbnails, - thumbnailActive, + currentDisplaySetInstanceUID, onThumbnailClick, onClickUntrack, }) => { @@ -26,7 +26,8 @@ const ThumbnailList = ({ imageSrc, imageAltText, }) => { - const isActive = thumbnailActive === displaySetInstanceUID; + const isActive = + currentDisplaySetInstanceUID === displaySetInstanceUID; switch (componentType) { case 'thumbnail': @@ -112,7 +113,7 @@ ThumbnailList.propTypes = { }), }) ), - thumbnailActive: PropTypes.string, + currentDisplaySetInstanceUID: PropTypes.string, onThumbnailClick: PropTypes.func, onClickUntrack: PropTypes.func.isRequired, }; From 520e1c077420355897a7465cbb3b1dacdb457b3b Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 16:05:18 -0300 Subject: [PATCH 10/24] fix focus styles --- platform/ui/src/components/Thumbnail/Thumbnail.jsx | 4 ++-- .../ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx | 2 +- platform/ui/tailwind.config.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.jsx b/platform/ui/src/components/Thumbnail/Thumbnail.jsx index 2f6420f04..d5d242626 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.jsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.jsx @@ -34,7 +34,7 @@ const Thumbnail = ({ ref={drag} className={classnames( className, - 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none' + 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none group' )} onDoubleClick={onClick} role="button" @@ -45,7 +45,7 @@ const Thumbnail = ({ 'flex flex-1 items-center justify-center rounded-md bg-black text-base text-white overflow-hidden mb-2 min-h-32', isActive ? 'border-2 border-primary-light' - : 'border border-secondary-light hover:border-blue-300' + : 'border border-secondary-light group-focus:border-blue-300 hover:border-blue-300' )} > {imageSrc ? ( diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 051d4c1df..892d371a7 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -24,7 +24,7 @@ const ThumbnailNoImage = ({
Date: Tue, 30 Jun 2020 15:20:14 -0400 Subject: [PATCH 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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; From 0f78ae3b57751bffcbb2cce0be4f891652f445fb Mon Sep 17 00:00:00 2001 From: Rodrigo Antinarelli Date: Tue, 30 Jun 2020 17:53:36 -0300 Subject: [PATCH 17/24] fix: remove focus if clicked outside thumbnail --- .../ui/src/components/Thumbnail/Thumbnail.jsx | 60 ++++++++++--------- .../ThumbnailNoImage/ThumbnailNoImage.jsx | 28 +++++---- platform/ui/src/utils/blurHandlerListener.js | 10 ++++ 3 files changed, 60 insertions(+), 38 deletions(-) create mode 100644 platform/ui/src/utils/blurHandlerListener.js diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.jsx b/platform/ui/src/components/Thumbnail/Thumbnail.jsx index d5d242626..3c30bc53b 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.jsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.jsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useDrag } from 'react-dnd'; // import { Icon } from '@ohif/ui'; +import blurHandlerListener from '../../utils/blurHandlerListener'; /** * @@ -29,9 +30,12 @@ const Thumbnail = ({ }, }); + const thumbnailElement = useRef(null); + return (
blurHandlerListener(thumbnailElement)} + ref={thumbnailElement} className={classnames( className, 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none group' @@ -40,34 +44,36 @@ const Thumbnail = ({ role="button" tabIndex="0" > -
- {imageSrc ? ( - {imageAltText} - ) : ( -
{imageAltText}
- )} -
-
-
- {'S: '} - {seriesNumber} +
+
+ {imageSrc ? ( + {imageAltText} + ) : ( +
{imageAltText}
+ )}
-
- {numInstances} +
+
+ {'S: '} + {seriesNumber} +
+
+ {numInstances} +
+
{description}
-
{description}
); }; diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 892d371a7..2ea1546a3 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useRef } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { useDrag } from 'react-dnd'; import { Icon } from '@ohif/ui'; +import blurHandlerListener from '../../utils/blurHandlerListener'; const ThumbnailNoImage = ({ description, @@ -20,9 +21,12 @@ const ThumbnailNoImage = ({ }, }); + const thumbnailElement = useRef(null); + return (
blurHandlerListener(thumbnailElement)} className={classnames( 'flex flex-row flex-1 px-4 py-3 cursor-pointer outline-none border-transparent hover:border-blue-300 focus:border-blue-300 rounded', isActive ? 'border-2 border-primary-light' : 'border' @@ -31,16 +35,18 @@ const ThumbnailNoImage = ({ role="button" tabIndex="0" > -
-
- -
- {modality} +
+
+
+ +
+ {modality} +
+ {seriesDate} +
+
+ {description}
- {seriesDate} -
-
- {description}
diff --git a/platform/ui/src/utils/blurHandlerListener.js b/platform/ui/src/utils/blurHandlerListener.js new file mode 100644 index 000000000..4ffba68c6 --- /dev/null +++ b/platform/ui/src/utils/blurHandlerListener.js @@ -0,0 +1,10 @@ +export default element => { + const handleClickOutside = event => { + if (element.current && !element.current.contains(event.target)) { + element.current.blur(); + document.removeEventListener('mousedown', handleClickOutside); + } + }; + + document.addEventListener('mousedown', handleClickOutside); +}; From b967d064f01350dcee3bb9b6d18cf72e4ce625ce Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 20:49:37 -0400 Subject: [PATCH 18/24] Tidying up naming --- .../PanelStudyBrowserTracking.jsx | 13 +++++++------ .../components/StudyBrowser/StudyBrowser.jsx | 10 +++++++--- .../ui/src/components/Thumbnail/Thumbnail.jsx | 8 +++++--- .../ThumbnailList/ThumbnailList.jsx | 19 +++++++++++++++---- .../ThumbnailNoImage/ThumbnailNoImage.jsx | 7 ++++--- .../ThumbnailTracked/ThumbnailTracked.jsx | 5 +++++ .../contextProviders/ViewportGridProvider.jsx | 16 +++++++++------- 7 files changed, 52 insertions(+), 26 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 1b717d20f..4a727ce48 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -23,7 +23,7 @@ function PanelStudyBrowserTracking({ // Tabs --> Studies --> DisplaySets --> Thumbnails const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer(); const [ - { activeViewportIndex, viewports, currentDisplaySetData }, + { activeViewportIndex, viewports }, viewportGridService, ] = useViewportGrid(); const [ @@ -39,15 +39,15 @@ function PanelStudyBrowserTracking({ const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); - const onClickThumbnailHandler = displaySetInstanceUID => { + const onDoubleClickThumbnailHandler = displaySetInstanceUID => { viewportGridService.setDisplaysetForViewport({ viewportIndex: activeViewportIndex, displaySetInstanceUID, }); }; - const currentDisplaySetInstanceUID = - currentDisplaySetData.displaySetInstanceUID || ''; + const activeDisplaySetInstanceUID = + viewports[activeViewportIndex]?.displaySetInstanceUID; // TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy? useEffect(() => { @@ -300,8 +300,9 @@ function PanelStudyBrowserTracking({ SeriesInstanceUID: displaySet.SeriesInstanceUID, }); }} - onClickThumbnail={onClickThumbnailHandler} - currentDisplaySetInstanceUID={currentDisplaySetInstanceUID} + onClickThumbnail={() => {}} + onDoubleClickThumbnail={onDoubleClickThumbnailHandler} + activeDisplaySetInstanceUID={activeDisplaySetInstanceUID} /> ); } diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx index 24b2f52f3..c5110f5c7 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx @@ -25,8 +25,9 @@ const StudyBrowser = ({ onClickTab, onClickStudy, onClickThumbnail, + onDoubleClickThumbnail, onClickUntrack, - currentDisplaySetInstanceUID, + activeDisplaySetInstanceUID, }) => { const getTabContent = () => { const tabData = tabs.find(tab => tab.name === activeTabName); @@ -57,8 +58,9 @@ const StudyBrowser = ({ {isExpanded && displaySets && ( )} @@ -108,10 +110,11 @@ StudyBrowser.propTypes = { onClickTab: PropTypes.func.isRequired, onClickStudy: PropTypes.func, onClickThumbnail: PropTypes.func, + onDoubleClickThumbnail: PropTypes.func, onClickUntrack: PropTypes.func, activeTabName: PropTypes.string.isRequired, expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired, - currentDisplaySetInstanceUID: PropTypes.string.isRequired, + activeDisplaySetInstanceUID: PropTypes.string, tabs: PropTypes.arrayOf( PropTypes.shape({ name: PropTypes.string.isRequired, @@ -164,6 +167,7 @@ StudyBrowser.defaultProps = { onClickTab: noop, onClickStudy: noop, onClickThumbnail: noop, + onDoubleClickThumbnail: noop, onClickUntrack: noop, }; diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.jsx b/platform/ui/src/components/Thumbnail/Thumbnail.jsx index 3d0f4c9d1..ec378ae09 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.jsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.jsx @@ -1,8 +1,7 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import { useDrag } from 'react-dnd'; -// import { Icon } from '@ohif/ui'; import blurHandlerListener from '../../utils/blurHandlerListener'; @@ -20,6 +19,7 @@ const Thumbnail = ({ dragData, isActive, onClick, + onDoubleClick, }) => { // TODO: We should wrap our thumbnail to create a "DraggableThumbnail", as // this will still allow for "drag", even if there is no drop target for the @@ -42,7 +42,8 @@ const Thumbnail = ({ 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none group' )} id={`thumbnail-${displaySetInstanceUID}`} - onDoubleClick={onClick} + onClick={onClick} + onDoubleClick={onDoubleClick} role="button" tabIndex="0" > @@ -101,6 +102,7 @@ Thumbnail.propTypes = { numInstances: PropTypes.number.isRequired, isActive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, }; Thumbnail.defaultProps = { diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index 0c37b571f..80e87f667 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -5,8 +5,9 @@ import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui'; const ThumbnailList = ({ thumbnails, - currentDisplaySetInstanceUID, + activeDisplaySetInstanceUID, onThumbnailClick, + onThumbnailDoubleClick, onClickUntrack, }) => { return ( @@ -27,7 +28,7 @@ const ThumbnailList = ({ imageAltText, }) => { const isActive = - currentDisplaySetInstanceUID === displaySetInstanceUID; + activeDisplaySetInstanceUID === displaySetInstanceUID; switch (componentType) { case 'thumbnail': @@ -44,6 +45,9 @@ const ThumbnailList = ({ viewportIdentificator={viewportIdentificator} isActive={isActive} onClick={() => onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => + onThumbnailDoubleClick(displaySetInstanceUID) + } /> ); case 'thumbnailTracked': @@ -61,6 +65,9 @@ const ThumbnailList = ({ isTracked={isTracked} isActive={isActive} onClick={() => onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => + onThumbnailDoubleClick(displaySetInstanceUID) + } onClickUntrack={() => onClickUntrack(displaySetInstanceUID)} /> ); @@ -75,6 +82,9 @@ const ThumbnailList = ({ seriesDate={seriesDate} description={description} onClick={() => onThumbnailClick(displaySetInstanceUID)} + onDoubleClick={() => + onThumbnailDoubleClick(displaySetInstanceUID) + } /> ); default: @@ -116,8 +126,9 @@ ThumbnailList.propTypes = { }), }) ), - currentDisplaySetInstanceUID: PropTypes.string, - onThumbnailClick: PropTypes.func, + activeDisplaySetInstanceUID: PropTypes.string, + onThumbnailClick: PropTypes.func.isRequired, + onThumbnailDoubleClick: PropTypes.func.isRequired, onClickUntrack: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 7c40ad5b0..c01e619f3 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -1,9 +1,7 @@ import React, { useRef } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; -import classnames from 'classnames'; import { useDrag } from 'react-dnd'; - import { Icon } from '@ohif/ui'; import blurHandlerListener from '../../utils/blurHandlerListener'; @@ -13,6 +11,7 @@ const ThumbnailNoImage = ({ seriesDate, modality, onClick, + onDoubleClick, dragData, isActive, }) => { @@ -34,7 +33,8 @@ const ThumbnailNoImage = ({ isActive ? 'border-2 border-primary-light' : 'border' )} id={`thumbnail-${displaySetInstanceUID}`} - onDoubleClick={onClick} + onClick={onClick} + onDoubleClick={onDoubleClick} role="button" tabIndex="0" > @@ -73,6 +73,7 @@ ThumbnailNoImage.propTypes = { modality: PropTypes.string.isRequired, seriesDate: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, isActive: PropTypes.bool.isRequired, }; diff --git a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx index 6715a77d3..2d8e09e05 100644 --- a/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx +++ b/platform/ui/src/components/ThumbnailTracked/ThumbnailTracked.jsx @@ -14,6 +14,7 @@ const ThumbnailTracked = ({ numInstances, dragData, onClick, + onDoubleClick, onClickUntrack, viewportIdentificator, isTracked, @@ -72,6 +73,7 @@ const ThumbnailTracked = ({ )}
); @@ -97,6 +100,7 @@ ThumbnailTracked.propTypes = { /** Must match the "type" a dropTarget expects */ type: PropTypes.string.isRequired, }), + displaySetInstanceUID: PropTypes.string.isRequired, className: PropTypes.string, imageSrc: PropTypes.string, imageAltText: PropTypes.string, @@ -104,6 +108,7 @@ ThumbnailTracked.propTypes = { seriesNumber: PropTypes.number.isRequired, numInstances: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, + onDoubleClick: PropTypes.func.isRequired, onClickUntrack: PropTypes.func.isRequired, viewportIdentificator: PropTypes.string, isTracked: PropTypes.bool, diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index eb8d9809c..a522ea165 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -10,9 +10,12 @@ import PropTypes from 'prop-types'; const DEFAULT_STATE = { numRows: 1, numCols: 1, - viewports: [], + viewports: [ + // { + // displaySetInstanceUID: string, + // } + ], activeViewportIndex: 0, - currentDisplaySetData: {}, }; export const ViewportGridContext = createContext(DEFAULT_STATE); @@ -21,16 +24,15 @@ export function ViewportGridProvider({ children, service }) { const viewportGridReducer = (state, action) => { switch (action.type) { case 'SET_ACTIVE_VIEWPORT_INDEX': { - const activeViewportIndex = action.payload; - const currentDisplaySetData = state.viewports[activeViewportIndex]; - return { ...state, ...{ activeViewportIndex, currentDisplaySetData } }; + return { ...state, ...{ activeViewportIndex: action.payload } }; } case 'SET_DISPLAYSET_FOR_VIEWPORT': { const { viewportIndex, displaySetInstanceUID } = action.payload; const viewports = state.viewports.slice(); - const currentDisplaySetData = { displaySetInstanceUID }; + viewports[viewportIndex] = { displaySetInstanceUID }; - return { ...state, ...{ viewports, currentDisplaySetData } }; + + return { ...state, ...{ viewports } }; } case 'SET_LAYOUT': { const { numCols, numRows } = action.payload; From 7f6aa0e26043df3e4f329470c2c548d4e70d334a Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 21:55:59 -0300 Subject: [PATCH 19/24] Update dependency: cornerstone-tools@4.16.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cornerstone-tools * @ohif/core: 4.16.0 → 4.16.1 * @ohif/extension-cornerstone: 4.16.0 → 4.16.1 * @ohif/extension-dicom-rt: 4.16.0 → 4.16.1 * @ohif/extension-dicom-segmentation: 4.15.1 → 4.16.1 * @ohif/extension-dicom-sr: 4.16.0 → 4.16.1 * @ohif/extension-measurement-tracking: 4.15.1 → 4.16.1 * @ohif/extension-vtk: 4.16.0 → 4.16.1 * @ohif/viewer: 4.16.0 → 4.16.1 --- extensions/cornerstone/package.json | 4 ++-- extensions/dicom-rt/package.json | 2 +- extensions/dicom-segmentation/package.json | 2 +- extensions/dicom-sr/package.json | 4 ++-- extensions/measurement-tracking/package.json | 4 ++-- extensions/vtk/package.json | 2 +- platform/core/package.json | 2 +- platform/viewer/package.json | 4 ++-- yarn.lock | 8 ++++---- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 6865c3beb..904f1f0b8 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -33,9 +33,9 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.16.0", - "dcmjs": "0.14.0", + "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", + "dcmjs": "0.14.0", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/dicom-rt/package.json b/extensions/dicom-rt/package.json index 9b7fc3c38..323ea25d8 100644 --- a/extensions/dicom-rt/package.json +++ b/extensions/dicom-rt/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", - "cornerstone-tools": "4.16.0", + "cornerstone-tools": "4.16.1", "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.8.6", diff --git a/extensions/dicom-segmentation/package.json b/extensions/dicom-segmentation/package.json index e9214776f..9306becf7 100644 --- a/extensions/dicom-segmentation/package.json +++ b/extensions/dicom-segmentation/package.json @@ -30,7 +30,7 @@ "peerDependencies": { "@ohif/core": "^0.50.0", "cornerstone-core": "^2.2.8", - "cornerstone-tools": "4.15.1", + "cornerstone-tools": "4.16.1", "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.8.6", diff --git a/extensions/dicom-sr/package.json b/extensions/dicom-sr/package.json index 7ea514c52..731b7d39c 100644 --- a/extensions/dicom-sr/package.json +++ b/extensions/dicom-sr/package.json @@ -33,9 +33,9 @@ "@ohif/ui": "^0.50.0", "cornerstone-core": "^2.3.0", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.16.0", - "dcmjs": "0.14.0", + "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", + "dcmjs": "0.14.0", "dicom-parser": "^1.8.3", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/measurement-tracking/package.json b/extensions/measurement-tracking/package.json index 86bad62c0..1ed3dbb5b 100644 --- a/extensions/measurement-tracking/package.json +++ b/extensions/measurement-tracking/package.json @@ -28,12 +28,12 @@ }, "peerDependencies": { "@ohif/core": "^0.50.0", + "cornerstone-tools": "4.16.1", "dcmjs": "0.14.0", "prop-types": "^15.6.2", "react": "^16.13.1", "react-dom": "^16.13.1", - "webpack": "^4.0.0", - "cornerstone-tools": "4.15.1" + "webpack": "^4.0.0" }, "dependencies": { "@babel/runtime": "7.7.6", diff --git a/extensions/vtk/package.json b/extensions/vtk/package.json index 5ef625611..3e33aa678 100644 --- a/extensions/vtk/package.json +++ b/extensions/vtk/package.json @@ -55,9 +55,9 @@ }, "devDependencies": { "@ohif/core": "^2.9.6", - "cornerstone-tools": "4.16.0", "@ohif/ui": "^2.0.0", "cornerstone-core": "^2.3.0", + "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3", "gh-pages": "^2.0.1", diff --git a/platform/core/package.json b/platform/core/package.json index 34aa934cc..3a16b5b95 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -31,7 +31,7 @@ }, "peerDependencies": { "cornerstone-core": "^2.3.0", - "cornerstone-tools": "4.16.0", + "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", "dicom-parser": "^1.8.3" }, diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 9364101ef..629e5e304 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -55,9 +55,9 @@ "@ohif/extension-dicom-html": "^1.1.0", "@ohif/extension-dicom-microscopy": "^0.50.6", "@ohif/extension-dicom-pdf": "^1.0.1", + "@ohif/extension-dicom-sr": "^0.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", @@ -67,7 +67,7 @@ "classnames": "^2.2.6", "core-js": "^3.2.1", "cornerstone-math": "^0.1.8", - "cornerstone-tools": "4.16.0", + "cornerstone-tools": "4.16.1", "cornerstone-wado-image-loader": "^3.1.2", "dcmjs": "0.14.0", "dicom-parser": "^1.8.3", diff --git a/yarn.lock b/yarn.lock index 7cb2da6e7..ee59610f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6755,10 +6755,10 @@ cornerstone-math@^0.1.8: resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.8.tgz#68ab1f9e4fdcd7c5cb23a0d2eb4263f9f894f1c5" integrity sha512-x7NEQHBtVG7j1yeyj/aRoKTpXv1Vh2/H9zNLMyqYJDtJkNng8C4Q8M3CgZ1qer0Yr7eVq2x+Ynmj6kfOm5jXKw== -cornerstone-tools@4.16.0: - version "4.16.0" - resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.16.0.tgz#af3d32d13722b97bec258492642e622312280196" - integrity sha512-kUhuSb2Ixpd2hgbdem+740rnN4hmoxzcOBNaUcsizFRWjMAsgc0yUxBFwLl0mIs811mefq79JOL+juwRA8T3Tg== +cornerstone-tools@4.16.1: + version "4.16.1" + resolved "https://registry.yarnpkg.com/cornerstone-tools/-/cornerstone-tools-4.16.1.tgz#e38f471c8fd30c6d25aab9a7995914542f3a87c5" + integrity sha512-c5gww9Px97R/avFXlS93uyr93syFbhHAHemEykO2Ot/hVQN8EDQTOmsjR/mRRSnVdLlNug4w16obLtWWAl23KQ== dependencies: "@babel/runtime" "7.1.2" cornerstone-math "0.1.7" From 4b58d925d9c3e599fb8425d9399c1e815170bb88 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 22:08:41 -0400 Subject: [PATCH 20/24] Use ImageViewer's `StudyInstanceUIDs` to set the initial/default list of expanded study ids --- extensions/default/src/Panels/PanelStudyBrowser.jsx | 8 +++----- .../PanelStudyBrowserTracking.jsx | 10 +++++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index 2d66fc634..43df658a2 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -21,15 +21,13 @@ function PanelStudyBrowser({ // Tabs --> Studies --> DisplaySets --> Thumbnails const [{ StudyInstanceUIDs }, dispatch] = useImageViewer(); const [activeTabName, setActiveTabName] = useState('primary'); - const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState( - [] - ); + const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ + ...StudyInstanceUIDs, + ]); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); - console.log(DisplaySetService); - // ~~ studyDisplayList useEffect(() => { // Fetch all studies for the patient in each primary study diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 4a727ce48..6616c6306 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -31,9 +31,9 @@ function PanelStudyBrowserTracking({ sendTrackedMeasurementsEvent, ] = useTrackedMeasurements(); const [activeTabName, setActiveTabName] = useState('primary'); - const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState( - [] - ); + const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([ + ...StudyInstanceUIDs, + ]); const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); @@ -308,6 +308,10 @@ function PanelStudyBrowserTracking({ } PanelStudyBrowserTracking.propTypes = { + MeasurementService: PropTypes.shape({ + subscribe: PropTypes.func.isRequired, + EVENTS: PropTypes.object.isRequired, + }).isRequired, DisplaySetService: PropTypes.shape({ EVENTS: PropTypes.object.isRequired, activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired, From 9fb3fe6dc2cf2894d3d44c5dea97445225697380 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 22:32:54 -0400 Subject: [PATCH 21/24] Shift create report to a separate file --- .../src/_shared/createReportAsync.js | 47 +++++++++++++ .../PanelMeasurementTableTracking/index.js | 69 +++++-------------- 2 files changed, 63 insertions(+), 53 deletions(-) create mode 100644 extensions/measurement-tracking/src/_shared/createReportAsync.js diff --git a/extensions/measurement-tracking/src/_shared/createReportAsync.js b/extensions/measurement-tracking/src/_shared/createReportAsync.js new file mode 100644 index 000000000..326ad249b --- /dev/null +++ b/extensions/measurement-tracking/src/_shared/createReportAsync.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { DICOMSR } from '@ohif/core'; + +async function createReportAsync(servicesManager, dataSource, measurements) { + const { + UINotificationService, + UIDialogService, + DisplaySetService, + } = servicesManager.services; + const loadingDialogId = UIDialogService.create({ + showOverlay: true, + isDraggable: false, + centralize: true, + // TODO: Create a loading indicator component + zeplin design? + content: Loading, + }); + + try { + const naturalizedReport = await DICOMSR.storeMeasurements( + measurements, + dataSource + ); + + DisplaySetService.makeDisplaySets([naturalizedReport], { + madeInClient: true, + }); + UINotificationService.show({ + title: 'STOW SR', + message: 'Measurements saved successfully', + type: 'success', + }); + } catch (error) { + UINotificationService.show({ + title: 'STOW SR', + message: error.message || 'Failed to store measurements', + type: 'error', + }); + } finally { + UIDialogService.dismiss({ id: loadingDialogId }); + } +} + +function Loading() { + return
Loading...
; +} + +export default createReportAsync; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index e8381f39f..620c67e31 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -5,6 +5,7 @@ import { DicomMetadataStore, DICOMSR } from '@ohif/core'; import { useDebounce } from '@hooks'; import ActionButtons from './ActionButtons'; import { useTrackedMeasurements } from '../../getContextModule'; +import createReportAsync from './../../_shared/createReportAsync.js'; const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { key: undefined, // @@ -21,12 +22,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { - MeasurementService, - UINotificationService, - UIDialogService, - DisplaySetService, - } = servicesManager.services; + const { MeasurementService } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -103,7 +99,20 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { }; }, [MeasurementService, sendTrackedMeasurementsEvent]); - const activeMeasurementItem = 0; + function createReport() { + // TODO -> Eventually deal with multiple dataSources. + // Would need some way of saying which one is the "push" dataSource + const dataSources = extensionManager.getDataSources(); + const dataSource = dataSources[0]; + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); + + return createReportAsync(servicesManager, dataSource, trackedMeasurements); + } const exportReport = () => { const measurements = MeasurementService.getMeasurements(); @@ -117,52 +126,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DICOMSR.downloadReport(trackedMeasurements, dataSource); }; - const createReport = async () => { - const loadingDialogId = UIDialogService.create({ - showOverlay: true, - isDraggable: false, - centralize: true, - // TODO: Create a loading indicator component + zeplin design? - content: () =>
Loading...
, - }); - - try { - const measurements = MeasurementService.getMeasurements(); - const trackedMeasurements = measurements.filter( - m => - trackedStudy === m.referenceStudyUID && - trackedSeries.includes(m.referenceSeriesUID) - ); - - const dataSources = extensionManager.getDataSources(); - // TODO -> Eventually deal with multiple dataSources. - // Would need some way of saying which one is the "push" dataSource - const dataSource = dataSources[0]; - - const naturalizedReport = await DICOMSR.storeMeasurements( - trackedMeasurements, - dataSource - ); - - DisplaySetService.makeDisplaySets([naturalizedReport], { - madeInClient: true, - }); - UINotificationService.show({ - title: 'STOW SR', - message: 'Measurements saved successfully', - type: 'success', - }); - } catch (error) { - UINotificationService.show({ - title: 'STOW SR', - message: error.message || 'Failed to store measurements', - type: 'error', - }); - } finally { - UIDialogService.dismiss({ id: loadingDialogId }); - } - }; - return ( <>
From c8f48cf1fe42066aac4c3d01c02363b3c7798398 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 22:54:09 -0400 Subject: [PATCH 22/24] pass servicesManager and extensionManager all the way to our UI prompts; use in exposed createReportAsync call --- .../TrackedMeasurementsContext.jsx | 38 ++++++++++++------- .../promptBeginTracking.js | 3 +- .../promptTrackNewSeries.js | 23 +++++++++-- .../promptTrackNewStudy.js | 23 +++++++++-- .../src/getContextModule.js | 5 +-- 5 files changed, 68 insertions(+), 24 deletions(-) diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx index 5081deaad..3ff87f663 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx @@ -19,25 +19,33 @@ const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext); * @param {*} param0 */ function TrackedMeasurementsContextProvider( - UIViewportDialogService, - { children } + { servicesManager, extensionManager }, // Bound by consumer + { children } // Component props ) { const machineOptions = Object.assign({}, defaultOptions); machineOptions.services = Object.assign({}, machineOptions.services, { - promptBeginTracking: promptBeginTracking.bind( - null, - UIViewportDialogService - ), - promptTrackNewSeries: promptTrackNewSeries.bind( - null, - UIViewportDialogService - ), - promptTrackNewStudy: promptTrackNewStudy.bind( - null, - UIViewportDialogService - ), + promptBeginTracking: promptBeginTracking.bind(null, { + servicesManager, + extensionManager, + }), + promptTrackNewSeries: promptTrackNewSeries.bind(null, { + servicesManager, + extensionManager, + }), + promptTrackNewStudy: promptTrackNewStudy.bind(null, { + servicesManager, + extensionManager, + }), }); + // TODO: IMPROVE + // - Add measurement_updated to cornerstone; debounced? (ext side, or consumption?) + // - Friendlier transition/api in front of measurementTracking machine? + // - Blocked: viewport overlay shouldn't clip when resized + // TODO: PRIORITY + // - Fix "ellipses" series description dynamic truncate length + // - Fix viewport border resize + // - created/destroyed hooks for extensions (cornerstone measurement subscriptions in it's `init`) const measurementTrackingMachine = Machine( machineConfiguration, @@ -61,6 +69,8 @@ function TrackedMeasurementsContextProvider( TrackedMeasurementsContextProvider.propTypes = { children: PropTypes.oneOf([PropTypes.func, PropTypes.node]), + servicesManager: PropTypes.object.isRequired, + extensionManager: PropTypes.object.isRequired, }; export { diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js index 657c24e66..57a6188fd 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js @@ -6,7 +6,8 @@ const RESPONSE = { SET_STUDY_AND_SERIES: 3, }; -function promptUser(UIViewportDialogService, ctx, evt) { +function promptUser({ servicesManager }, ctx, evt) { + const { UIViewportDialogService } = servicesManager.services; const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; return new Promise(async function(resolve, reject) { diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js index 4a6f7dcc1..2370ef65d 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js @@ -1,3 +1,5 @@ +import createReportAsync from './../../_shared/createReportAsync.js'; + const RESPONSE = { NO_NEVER: -1, CANCEL: 0, @@ -6,8 +8,13 @@ const RESPONSE = { SET_STUDY_AND_SERIES: 3, }; -function promptUser(UIViewportDialogService, ctx, evt) { +function promptUser({ servicesManager, extensionManager }, ctx, evt) { + const { + UIViewportDialogService, + MeasurementService, + } = servicesManager.services; const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + const { trackedStudy, trackedSeries } = ctx; return new Promise(async function(resolve, reject) { let promptResult = await _askShouldAddMeasurements( @@ -22,9 +29,19 @@ function promptUser(UIViewportDialogService, ctx, evt) { ); } - // TODO: Hook into @JamesAPetts createReport if (promptResult === RESPONSE.CREATE_REPORT) { - window.alert('CREATE REPORT'); + // TODO -> Eventually deal with multiple dataSources. + // Would need some way of saying which one is the "push" dataSource + const dataSources = extensionManager.getDataSources(); + const dataSource = dataSources[0]; + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); + + createReportAsync(servicesManager, dataSource, trackedMeasurements); } resolve({ diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js index 0f8842a31..62513f0cd 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js @@ -1,3 +1,5 @@ +import createReportAsync from './../../_shared/createReportAsync.js'; + const RESPONSE = { NO_NEVER: -1, CANCEL: 0, @@ -6,8 +8,13 @@ const RESPONSE = { SET_STUDY_AND_SERIES: 3, }; -function promptUser(UIViewportDialogService, ctx, evt) { +function promptUser({ servicesManager, extensionManager }, ctx, evt) { + const { + UIViewportDialogService, + MeasurementService, + } = servicesManager.services; const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + const { trackedStudy, trackedSeries } = ctx; return new Promise(async function(resolve, reject) { let promptResult = await _askTrackMeasurements( @@ -22,9 +29,19 @@ function promptUser(UIViewportDialogService, ctx, evt) { ); } - // TODO: Hook into @JamesAPetts createReport if (promptResult === RESPONSE.CREATE_REPORT) { - window.alert('CREATE REPORT'); + // TODO -> Eventually deal with multiple dataSources. + // Would need some way of saying which one is the "push" dataSource + const dataSources = extensionManager.getDataSources(); + const dataSource = dataSources[0]; + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); + + createReportAsync(servicesManager, dataSource, trackedMeasurements); } resolve({ diff --git a/extensions/measurement-tracking/src/getContextModule.js b/extensions/measurement-tracking/src/getContextModule.js index 573a00fa2..29acdc402 100644 --- a/extensions/measurement-tracking/src/getContextModule.js +++ b/extensions/measurement-tracking/src/getContextModule.js @@ -4,11 +4,10 @@ import { useTrackedMeasurements, } from './contexts'; -function getContextModule({ servicesManager }) { - const { UIViewportDialogService } = servicesManager.services; +function getContextModule({ servicesManager, extensionManager }) { const BoundTrackedMeasurementsContextProvider = TrackedMeasurementsContextProvider.bind( null, - UIViewportDialogService + { servicesManager, extensionManager } ); return [ From 7b31e62e3182d5776a0e56aa49ca5fb40b7d9c80 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 22:54:36 -0400 Subject: [PATCH 23/24] Fix undefined data source for local download/export --- .../src/panels/PanelMeasurementTableTracking/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 620c67e31..1200d2dfa 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -114,7 +114,9 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { return createReportAsync(servicesManager, dataSource, trackedMeasurements); } - const exportReport = () => { + function exportReport() { + const dataSources = extensionManager.getDataSources(); + const dataSource = dataSources[0]; const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => @@ -124,7 +126,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { // TODO -> local download. DICOMSR.downloadReport(trackedMeasurements, dataSource); - }; + } return ( <> From bc2bde890b39a36cdeaabd96dc15c606c06b8552 Mon Sep 17 00:00:00 2001 From: James Petts Date: Wed, 1 Jul 2020 10:08:56 +0100 Subject: [PATCH 24/24] OHIF-190 - Rehydrate PR (#1835) * WIP rehydration. * WIP OHIF-190 rehydrate. * Remove debugger statements. * Merge in and fix after OHIF-197 and OHIF-198 * Remove console.log * Fix typo * fix length tracking bug * Respond to reviewer comments. --- extensions/cornerstone/package.json | 2 +- extensions/cornerstone/src/init.js | 2 + .../ArrowAnnotate.js | 2 +- .../Bidirectional.js | 2 +- .../EllipticalRoi.js | 2 +- .../measurementServiceMappings/Length.js | 7 +- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 214 ++++++++++++++++-- .../dicom-sr/src/getSopClassHandlerModule.js | 32 ++- extensions/dicom-sr/src/index.js | 19 +- .../dicom-sr/src/utils/addMeasurement.js | 6 + ...ToolStateToCornerstoneMeasurementSchema.js | 202 +++++++++++++++++ .../dicom-sr/src/utils/isRehydratable.js | 48 ++++ .../measurementTrackingMachine.js | 16 ++ .../viewports/TrackedCornerstoneViewport.js | 6 +- .../MeasurementService/MeasurementService.js | 113 +++++++++ .../ViewportActionBar/ViewportActionBar.jsx | 43 +++- .../viewer/src/components/ViewportGrid.jsx | 4 +- 17 files changed, 674 insertions(+), 46 deletions(-) create mode 100644 extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js create mode 100644 extensions/dicom-sr/src/utils/isRehydratable.js diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index 904f1f0b8..226be4bc4 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -1,6 +1,6 @@ { "name": "@ohif/extension-cornerstone", - "version": "2.7.3", + "version": "3.0.0", "description": "OHIF extension for Cornerstone", "author": "OHIF", "license": "MIT", diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index e7b321935..319a4c484 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -246,10 +246,12 @@ const _connectToolsToMeasurementService = measurementService => { function addMeasurement(csToolsEvent) { console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail); + try { const evtDetail = csToolsEvent.detail; const { toolName, toolType, measurementData } = evtDetail; const csToolName = toolName || measurementData.toolType || toolType; + const measurementId = addOrUpdate(csToolName, evtDetail); if (measurementId) { diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js index 7d2c1c3b2..f36c64ad8 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.js @@ -30,7 +30,7 @@ const ArrowAnnotate = { points.push(measurementData.handles); return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js index 5b6f3e893..25ca59b12 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.js @@ -31,7 +31,7 @@ const Bidirectional = { const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js index dbd4d2403..b2a24187e 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalRoi.js @@ -53,7 +53,7 @@ const EllipticalRoi = { } return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js index 5f9eda649..a5b7c1e9d 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.js @@ -26,7 +26,7 @@ const Length = { text: label, description, handles: getHandlesFromPoints(points), - _measurementServiceId: id, + id, }, }; }, @@ -57,11 +57,8 @@ const Length = { StudyInstanceUID, } = getSOPInstanceAttributes(element); - const points = []; - points.push(measurementData.handles); - return { - id: measurementData._measurementServiceId, + id: measurementData.id, SOPInstanceUID: SOPInstanceUID, FrameOfReferenceUID, referenceSeriesUID: SeriesInstanceUID, diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index b5b7caaf2..5f71ce062 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -1,40 +1,63 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import cornerstoneTools from 'cornerstone-tools'; import cornerstone from 'cornerstone-core'; - import CornerstoneViewport from 'react-cornerstone-viewport'; -import OHIF, { utils } from '@ohif/core'; +import OHIF, { DicomMetadataStore, utils } from '@ohif/core'; import { ViewportActionBar, useViewportGrid } from '@ohif/ui'; import TOOL_NAMES from './constants/toolNames'; +import { adapters } from 'dcmjs'; +import getToolStateToCornerstoneMeasurementSchema from './utils/getToolStateToCornerstoneMeasurementSchema'; import id from './id'; const { formatDate } = utils; const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex'); +const globalImageIdSpecificToolStateManager = + cornerstoneTools.globalImageIdSpecificToolStateManager; -// const cine = viewportSpecificData.cine; +const { StackManager, guid } = OHIF.utils; -// isPlaying = cine.isPlaying === true; -// frameRate = cine.cineFrameRate || frameRate; - -const { StackManager } = OHIF.utils; +const MEASUREMENT_TRACKING_EXTENSION_ID = 'org.ohif.measurement-tracking'; function OHIFCornerstoneSRViewport({ children, dataSource, displaySet, viewportIndex, - DisplaySetService, + servicesManager, + extensionManager, }) { + const { DisplaySetService, MeasurementService } = servicesManager.services; const [viewportGrid, viewportGridService] = useViewportGrid(); const [measurementSelected, setMeasurementSelected] = useState(0); const [measurementCount, setMeasurementCount] = useState(1); const [viewportData, setViewportData] = useState(null); const [activeDisplaySetData, setActiveDisplaySetData] = useState({}); const [element, setElement] = useState(null); - + const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); const { viewports, activeViewportIndex } = viewportGrid; + // Optional hook into tracking extension, if present. + let trackedMeasurements; + let sendTrackedMeasurementsEvent; + + if ( + extensionManager.registeredExtensionIds.includes( + MEASUREMENT_TRACKING_EXTENSION_ID + ) + ) { + const contextModule = extensionManager.getModuleEntry( + 'org.ohif.measurement-tracking.contextModule.TrackedMeasurementsContext' + ); + + const useTrackedMeasurements = () => useContext(contextModule.context); + + [ + trackedMeasurements, + sendTrackedMeasurementsEvent, + ] = useTrackedMeasurements(); + } + const onElementEnabled = evt => { const eventData = evt.detail; const targetElement = eventData.element; @@ -196,8 +219,6 @@ function OHIFCornerstoneSRViewport({ const { Modality } = displaySet; - // TODO -> Get this from the associated stack. - const { PatientID, PatientName, @@ -210,6 +231,7 @@ function OHIFCornerstoneSRViewport({ SeriesInstanceUID, PixelSpacing, SeriesNumber, + displaySetInstanceUID, } = activeDisplaySetData; const onMeasurementChange = direction => { @@ -229,22 +251,158 @@ function OHIFCornerstoneSRViewport({ } } - if (newMeasurementSelected === measurementSelected) { - // TODO -> Jump to image in this case. - } - updateViewport(newMeasurementSelected); }; + function hydrateMeasurementService() { + // TODO -> We should define a strict versioning somewhere. + const mappings = MeasurementService.getSourceMappings( + 'CornerstoneTools', + '4' + ); + + if (!mappings || !mappings.length) { + throw new Error( + `Attempting to hydrate measurements service when no mappings present. This shouldn't be reached.` + ); + } + + const instance = DicomMetadataStore.getInstance( + displaySet.StudyInstanceUID, + displaySet.SeriesInstanceUID, + displaySet.SOPInstanceUID + ); + + const { MeasurementReport } = adapters.Cornerstone; + + const sopInstanceUIDToImageId = {}; + + displaySet.measurements.forEach(measurement => { + const { ReferencedSOPInstanceUID, imageId } = measurement; + if (!sopInstanceUIDToImageId[ReferencedSOPInstanceUID]) { + sopInstanceUIDToImageId[ReferencedSOPInstanceUID] = imageId; + } + }); + + // Use dcmjs to generate toolState. + const storedMeasurementByToolType = MeasurementReport.generateToolState( + instance + ); + + // Filter what is found by DICOM SR to measurements we support. + const mappingDefinitions = mappings.map(m => m.definition); + const hydratableMeasurementsInSR = {}; + + Object.keys(storedMeasurementByToolType).forEach(key => { + if (mappingDefinitions.includes(key)) { + hydratableMeasurementsInSR[key] = storedMeasurementByToolType[key]; + } + }); + + if ( + extensionManager.registeredExtensionIds.includes( + MEASUREMENT_TRACKING_EXTENSION_ID + ) + ) { + // Set the series touched as tracked. + const imageIds = []; + + Object.keys(hydratableMeasurementsInSR).forEach(toolType => { + const toolDataForToolType = hydratableMeasurementsInSR[toolType]; + + toolDataForToolType.forEach(data => { + // Add the measurement to toolState + const imageId = sopInstanceUIDToImageId[data.sopInstanceUid]; + + if (!imageIds.includes(imageId)) { + imageIds.push(imageId); + } + }); + }); + + let targetStudyInstanceUID; + const SeriesInstanceUIDs = []; + + for (let i = 0; i < imageIds.length; i++) { + const imageId = imageIds[0]; + const { + SeriesInstanceUID, + StudyInstanceUID, + } = cornerstone.metaData.get('instance', imageId); + + if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { + SeriesInstanceUIDs.push(SeriesInstanceUID); + } + + if (!targetStudyInstanceUID) { + targetStudyInstanceUID = StudyInstanceUID; + } else if (targetStudyInstanceUID !== StudyInstanceUID) { + console.warn( + 'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.' + ); + } + } + + sendTrackedMeasurementsEvent('SET_TRACKED_SERIES', { + StudyInstanceUID: targetStudyInstanceUID, + SeriesInstanceUIDs, + }); + } + + Object.keys(hydratableMeasurementsInSR).forEach(toolType => { + const toolDataForToolType = hydratableMeasurementsInSR[toolType]; + + toolDataForToolType.forEach(data => { + // Add the measurement to toolState + const imageId = sopInstanceUIDToImageId[data.sopInstanceUid]; + + data.id = guid(); + + _addToolDataToCornerstoneTools(data, toolType, imageId); + + // Let the measurement service know we added to toolState + const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema( + toolType, + MeasurementService, + imageId + ); + + const source = MeasurementService.getSource('CornerstoneTools', '4'); + + MeasurementService.addRawMeasurement( + source, + toolType, + data, + toMeasurementSchema + ); + + if (!imageIds.includes(imageId)) { + imageIds.push(imageId); + } + }); + }); + + displaySet.isHydrated = true; + + setIsHydrated(true); + + viewportGridService.setDisplaysetForViewport({ + viewportIndex: activeViewportIndex, + displaySetInstanceUID: activeDisplaySetData.displaySetInstanceUID, + }); + } + return ( <> { _checkIfCanAddMeasurementsToDisplaySet( @@ -346,6 +360,12 @@ function _processTID1410Measurement(mergedContentSequence) { group => group.ValueType === 'UIDREF' ); + const TrackingIdentifierContentItem = mergedContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.TrackingIdentifier + ); + if (!graphicItem) { console.warn( `graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.` @@ -362,6 +382,7 @@ function _processTID1410Measurement(mergedContentSequence) { labels: [], coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)], TrackingUniqueIdentifier: UIDREFContentItem.UID, + TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; NUMContentItems.forEach(item => { @@ -389,11 +410,18 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { group => group.ValueType === 'UIDREF' ); + const TrackingIdentifierContentItem = mergedContentSequence.find( + item => + item.ConceptNameCodeSequence.CodeValue === + CodeNameCodeSequenceValues.TrackingIdentifier + ); + const measurement = { loaded: false, labels: [], coords: [], TrackingUniqueIdentifier: UIDREFContentItem.UID, + TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; NUMContentItems.forEach(item => { diff --git a/extensions/dicom-sr/src/index.js b/extensions/dicom-sr/src/index.js index 8a2a6d98a..498f51517 100644 --- a/extensions/dicom-sr/src/index.js +++ b/extensions/dicom-sr/src/index.js @@ -23,6 +23,18 @@ export default { * Only required property. Should be a unique value across all extensions. */ id, + dependencies: [ + // TODO -> This isn't used anywhere yet, but we do have a hard dependency, and need to check for these in the future. + // OHIF-229 + { + id: 'org.ohif.cornerstone', + version: '3.0.0', + }, + { + id: 'org.ohif.measurement-tracking', + version: '^0.0.1', + }, + ], preRegistration({ servicesManager, configuration = {} }) { init({ servicesManager, configuration }); @@ -34,13 +46,12 @@ export default { * @param {object} [configuration={}] * @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools` */ - getViewportModule({ servicesManager }) { + getViewportModule({ servicesManager, extensionManager }) { const ExtendedOHIFCornerstoneSRViewport = props => { - const { DisplaySetService } = servicesManager.services; - return ( ); diff --git a/extensions/dicom-sr/src/utils/addMeasurement.js b/extensions/dicom-sr/src/utils/addMeasurement.js index 8ad925b2c..f3d28c317 100644 --- a/extensions/dicom-sr/src/utils/addMeasurement.js +++ b/extensions/dicom-sr/src/utils/addMeasurement.js @@ -62,6 +62,12 @@ export default function addMeasurement( measurement.loaded = true; measurement.imageId = imageId; measurement.displaySetInstanceUID = displaySetInstanceUID; + + // Remove the unneeded coord now its processed, but keep the SOPInstanceUID. + // NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame, + // It'd be super werid if it didn't anyway as a SCOORD. + measurement.ReferencedSOPInstanceUID = + measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID; delete measurement.coords; } diff --git a/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js b/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js new file mode 100644 index 000000000..d37b8c0be --- /dev/null +++ b/extensions/dicom-sr/src/utils/getToolStateToCornerstoneMeasurementSchema.js @@ -0,0 +1,202 @@ +export default function getToolStateToCornerstoneMeasurementSchema( + toolType, + MeasurementService, + imageId +) { + const _getValueTypeFromToolType = toolType => { + const { + POLYLINE, + ELLIPSE, + POINT, + BIDIRECTIONAL, + } = MeasurementService.VALUE_TYPES; + + // TODO -> I get why this was attemped, but its not nearly flexible enough. + // A single measurement may have an ellipse + a bidirectional measurement, for instances. + // You can't define a bidirectional tool as a single type.. + // OHIF-230 + const TOOL_TYPE_TO_VALUE_TYPE = { + Length: POLYLINE, + EllipticalRoi: ELLIPSE, + Bidirectional: BIDIRECTIONAL, + ArrowAnnotate: POINT, + }; + + return TOOL_TYPE_TO_VALUE_TYPE[toolType]; + }; + + switch (toolType) { + case 'Length': + return measurementData => + Length(measurementData, imageId, _getValueTypeFromToolType); + case 'Bidirectional': + return measurementData => + Bidirectional(measurementData, imageId, _getValueTypeFromToolType); + case 'EllipticalRoi': + return measurementData => + EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType); + case 'ArrowAnnotate': + return measurementData => + ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType); + } +} + +function Length(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { handles } = measurementData; + + const points = []; + Object.keys(handles).map(handle => { + if (['start', 'end'].includes(handle)) { + let point = {}; + if (handles[handle].x) point.x = handles[handle].x; + if (handles[handle].y) point.y = handles[handle].y; + points.push(point); + } + }); + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + length: measurementData.length, + type: _getValueTypeFromToolType(tool), + points, + }; +} + +function Bidirectional(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { handles } = measurementData; + + const longAxis = [handles.start, handles.end]; + const shortAxis = [handles.perpendicularStart, handles.perpendicularEnd]; + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + shortestDiameter: measurementData.shortestDiameter, + longestDiameter: measurementData.longestDiameter, + type: _getValueTypeFromToolType(tool), + points: { longAxis, shortAxis }, + }; +} + +function EllipticalRoi(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { start, end } = measurementData.handles; + + const halfXLength = Math.abs(start.x - end.x) / 2; + const halfYLength = Math.abs(start.y - end.y) / 2; + + const points = []; + const center = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }; + + // To store similar to SR. + if (halfXLength > halfYLength) { + // X-axis major + // Major axis + points.push({ x: center.x - halfXLength, y: center.y }); + points.push({ x: center.x + halfXLength, y: center.y }); + // Minor axis + points.push({ x: center.x, y: center.y - halfYLength }); + points.push({ x: center.x, y: center.y + halfYLength }); + } else { + // Y-axis major + // Major axis + points.push({ x: center.x, y: center.y - halfYLength }); + points.push({ x: center.x, y: center.y + halfYLength }); + // Minor axis + points.push({ x: center.x - halfXLength, y: center.y }); + points.push({ x: center.x + halfXLength, y: center.y }); + } + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + area: + measurementData.cachedStats && + measurementData.cachedStats + .area /* TODO: Add concept names instead (descriptor) */, + type: _getValueTypeFromToolType(tool), + points, + }; +} + +function ArrowAnnotate(measurementData, imageId, _getValueTypeFromToolType) { + const tool = measurementData.toolType || measurementData.toolName; + const instance = cornerstone.metaData.get('instance', imageId); + const { + SOPInstanceUID, + FrameOfReferenceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = instance; + + const { handles } = measurementData; + + const points = []; + Object.keys(handles).map(handle => { + if (['start', 'end'].includes(handle)) { + let point = {}; + if (handles[handle].x) point.x = handles[handle].x; + if (handles[handle].y) point.y = handles[handle].y; + points.push(point); + } + }); + + return { + id: measurementData.id, + SOPInstanceUID: SOPInstanceUID, + FrameOfReferenceUID, + referenceSeriesUID: SeriesInstanceUID, + referenceStudyUID: StudyInstanceUID, + label: measurementData.text, + description: measurementData.description, + unit: measurementData.unit, + text: measurementData.text, + type: _getValueTypeFromToolType(tool), + points, + }; +} diff --git a/extensions/dicom-sr/src/utils/isRehydratable.js b/extensions/dicom-sr/src/utils/isRehydratable.js new file mode 100644 index 000000000..750d184e0 --- /dev/null +++ b/extensions/dicom-sr/src/utils/isRehydratable.js @@ -0,0 +1,48 @@ +import { adapters } from 'dcmjs'; + +const cornerstoneAdapters = adapters.Cornerstone; + +/** + * Checks if the given `displySet`can be rehydrated into the `MeasurementService`. + * + * @param {object} displaySet The SR `displaySet` to check. + * @param {object[]} mappings The CornerstoneTools 4 mappings to the `MeasurementService`. + * @returns {boolean} True if the SR can be rehydrated into the `MeasurementService`. + */ +export default function isRehydratable(displaySet, mappings) { + if (!mappings || !mappings.length) { + return false; + } + + const mappingDefinitions = mappings.map(m => m.definition); + const { measurements } = displaySet; + + const adapterKeys = Object.keys(cornerstoneAdapters).filter( + adapterKey => + typeof cornerstoneAdapters[adapterKey] + .isValidCornerstoneTrackingIdentifier === 'function' + ); + + const adapters = []; + + adapterKeys.forEach(key => { + if (mappingDefinitions.includes(key)) { + // Must have both a dcmjs adapter and a MeasurementService + // Definition in order to be a candidate for import. + adapters.push(cornerstoneAdapters[key]); + } + }); + + for (let i = 0; i < measurements.length; i++) { + const TrackingIdentifier = measurements[i].TrackingIdentifier; + const hydratable = adapters.some(adapter => + adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier) + ); + + if (hydratable) { + return true; + } + } + + return false; +} diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 40ae9cac6..71c59eaf3 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -23,6 +23,12 @@ const machineConfiguration = { entry: 'clearContext', on: { TRACK_SERIES: 'promptBeginTracking', + SET_TRACKED_SERIES: [ + { + target: 'tracking', + actions: ['setTrackedStudyAndMultipleSeries'], + }, + ], }, }, promptBeginTracking: { @@ -69,6 +75,12 @@ const machineConfiguration = { target: 'idle', }, ], + SET_TRACKED_SERIES: [ + { + target: 'tracking', + actions: ['setTrackedStudyAndMultipleSeries'], + }, + ], }, }, promptTrackNewSeries: { @@ -139,6 +151,10 @@ const defaultOptions = { trackedStudy: evt.data.StudyInstanceUID, trackedSeries: [evt.data.SeriesInstanceUID], })), + setTrackedStudyAndMultipleSeries: assign((ctx, evt) => ({ + trackedStudy: evt.StudyInstanceUID, + trackedSeries: [...ctx.trackedSeries, ...evt.SeriesInstanceUIDs], + })), addTrackedSeries: assign((ctx, evt) => ({ trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID], })), diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 45dcf23ac..1b639282f 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -17,6 +17,10 @@ import ViewportOverlay from './ViewportOverlay'; const { formatDate } = utils; // TODO -> Get this list from the list of tracked measurements. +// TODO -> We can now get a list of tool names from the measurement service. +// Use the toolnames to check which tools we have instead, using the +// Classes isn't really extensible unless we add the classes to the measurement +// Service definition, which feels wrong. const { ArrowAnnotateTool, BidirectionalTool, @@ -40,7 +44,7 @@ function TrackedCornerstoneViewport({ dataSource, displaySet, viewportIndex, - ToolBarService + ToolBarService, }) { const [trackedMeasurements] = useTrackedMeasurements(); const [{ activeViewportIndex, viewports }] = useViewportGrid(); diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index ba9ddcf61..b97c09c55 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -155,6 +155,32 @@ class MeasurementService { return source; } + getSource(name, version) { + const { sources } = this; + const id = this._getSourceId(name, version); + + return sources[id]; + } + + getSourceMappings(name, version) { + const { mappings } = this; + const id = this._getSourceId(name, version); + + return mappings[id]; + } + + _getSourceId(name, version) { + const { sources } = this; + + const sourceId = Object.keys(sources).find(sourceId => { + const source = sources[sourceId]; + + return source.name === name && source.version === version; + }); + + return sourceId; + } + /** * Add a new measurement matching criteria along with mapping functions. * @@ -256,6 +282,93 @@ class MeasurementService { } } + /** + * Add 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. + * @param {MeasurementSource} source The measurement source instance. + * @param {string} definition The source definition you want to add the measuremnet to. + * @param {object} data The data you wish to add to the source. + * @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition. + */ + addRawMeasurement(source, definition, data, toMeasurementSchema) { + if (!this._isValidSource(source)) { + log.warn('Invalid source. Exiting early.'); + return; + } + + const sourceInfo = this._getSourceInfo(source); + + if (!definition) { + log.warn('No source definition provided. Exiting early.'); + return; + } + + if (!this._sourceHasMappings(source)) { + log.warn( + `No measurement mappings found for '${sourceInfo}' source. Exiting early.` + ); + return; + } + + let measurement = {}; + try { + /* Convert measurement */ + measurement = toMeasurementSchema(data); + + /* Assign measurement source instance */ + measurement.source = source; + } catch (error) { + log.warn( + `Failed to map '${sourceInfo}' measurement for definition ${definition}:`, + error.message + ); + return; + } + + if (!this._isValidMeasurement(measurement)) { + log.warn( + `Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.` + ); + return; + } + + let internalId = data.id; + if (!internalId) { + internalId = guid(); + log.warn(`Measurement ID not found. Generating UID: ${internalId}`); + } + + const newMeasurement = { + ...measurement, + modifiedTimestamp: Math.floor(Date.now() / 1000), + id: internalId, + }; + + if (this.measurements[internalId]) { + log.info( + `Measurement already defined. Updating measurement.`, + newMeasurement + ); + this.measurements[internalId] = newMeasurement; + this._broadcastChange( + this.EVENTS.MEASUREMENT_UPDATED, + source, + newMeasurement + ); + } else { + log.info(`Measurement added.`, newMeasurement); + this.measurements[internalId] = newMeasurement; + this._broadcastChange( + this.EVENTS.MEASUREMENT_ADDED, + source, + newMeasurement + ); + } + + return newMeasurement.id; + } + /** * Adds or update persisted measurements. * diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index d901edb6e..f856d98b8 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -15,13 +15,22 @@ const ViewportActionBar = ({ showNavArrows, showPatientInfo: patientInfoVisibility, onSeriesChange, + onHydrationClick, }) => { const [showPatientInfo, setShowPatientInfo] = useState(patientInfoVisibility); + // TODO -> Remake this component with a bunch of generic slots that can be filled, + // Its not generic at all, isTracked etc shouldn't be parts of this component. + // It shouldn't care that a tracking mode or SR exists. + // Things like the right/left buttons should be made into smaller + // Components you can compose. + // OHIF-200 ticket. + const { label, isTracked, isLocked, + isHydrated, modality, studyDate, currentSeries, @@ -44,18 +53,30 @@ const ViewportActionBar = ({ const renderIconStatus = () => { if (modality === 'SR') { return ( -
- - SR - - {isLocked && ( - + <> +
+ + SR + + {isLocked && ( + + )} +
+ {!isLocked && !isHydrated && ( +
+ + Edit + +
)} -
+ ); } diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index 4052cbd0a..7a92ce550 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -28,13 +28,13 @@ function ViewerViewportGrid(props) { displaySets => { displaySets.sort((a, b) => { const isImageSet = x => x instanceof ImageSet; - return (isImageSet(a) === isImageSet(b)) ? 0 : isImageSet(a) ? -1 : 1; + return isImageSet(a) === isImageSet(b) ? 0 : isImageSet(a) ? -1 : 1; }); viewportGridService.setDisplaysetForViewport({ viewportIndex: 0, displaySetInstanceUID: displaySets[0].displaySetInstanceUID, }); - }, + } ); return () => {